mirror of
https://github.com/hcengineering/platform.git
synced 2025-05-01 20:59:48 +00:00
Initial rest RPC (#8076)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
0d76e68516
commit
cd90f8bce6
@ -39,7 +39,8 @@
|
|||||||
"ts-node": "^10.8.0",
|
"ts-node": "^10.8.0",
|
||||||
"@types/node": "~20.11.16",
|
"@types/node": "~20.11.16",
|
||||||
"@types/jest": "^29.5.5",
|
"@types/jest": "^29.5.5",
|
||||||
"@types/ws": "^8.5.11"
|
"@types/ws": "^8.5.11",
|
||||||
|
"@types/snappyjs": "^0.7.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hcengineering/core": "^0.6.32",
|
"@hcengineering/core": "^0.6.32",
|
||||||
@ -48,7 +49,8 @@
|
|||||||
"@hcengineering/collaborator-client": "^0.6.4",
|
"@hcengineering/collaborator-client": "^0.6.4",
|
||||||
"@hcengineering/account-client": "^0.6.0",
|
"@hcengineering/account-client": "^0.6.0",
|
||||||
"@hcengineering/platform": "^0.6.11",
|
"@hcengineering/platform": "^0.6.11",
|
||||||
"@hcengineering/text": "^0.6.5"
|
"@hcengineering/text": "^0.6.5",
|
||||||
|
"snappyjs": "^0.7.0"
|
||||||
},
|
},
|
||||||
"repository": "https://github.com/hcengineering/platform",
|
"repository": "https://github.com/hcengineering/platform",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
@ -17,3 +17,4 @@ export * from './client'
|
|||||||
export * from './markup/types'
|
export * from './markup/types'
|
||||||
export * from './socket'
|
export * from './socket'
|
||||||
export * from './types'
|
export * from './types'
|
||||||
|
export * from './rest'
|
||||||
|
130
packages/api-client/src/rest.ts
Normal file
130
packages/api-client/src/rest.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
//
|
||||||
|
// Copyright © 2025 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 Account,
|
||||||
|
type Class,
|
||||||
|
type Doc,
|
||||||
|
type DocumentQuery,
|
||||||
|
type FindOptions,
|
||||||
|
type FindResult,
|
||||||
|
type Ref,
|
||||||
|
type Storage,
|
||||||
|
type Tx,
|
||||||
|
type TxResult,
|
||||||
|
type WithLookup,
|
||||||
|
concatLink
|
||||||
|
} from '@hcengineering/core'
|
||||||
|
|
||||||
|
import { PlatformError, unknownError } from '@hcengineering/platform'
|
||||||
|
|
||||||
|
import { uncompress } from 'snappyjs'
|
||||||
|
|
||||||
|
export interface RestClient extends Storage {
|
||||||
|
getAccount: () => Promise<Account>
|
||||||
|
|
||||||
|
findOne: <T extends Doc>(
|
||||||
|
_class: Ref<Class<T>>,
|
||||||
|
query: DocumentQuery<T>,
|
||||||
|
options?: FindOptions<T>
|
||||||
|
) => Promise<WithLookup<T> | undefined>
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createRestClient (endpoint: string, workspaceId: string, token: string): Promise<RestClient> {
|
||||||
|
return new RestClientImpl(endpoint, workspaceId, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
class RestClientImpl implements RestClient {
|
||||||
|
constructor (
|
||||||
|
readonly endpoint: string,
|
||||||
|
readonly workspace: string,
|
||||||
|
readonly token: string
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async findAll<T extends Doc>(
|
||||||
|
_class: Ref<Class<T>>,
|
||||||
|
query: DocumentQuery<T>,
|
||||||
|
options?: FindOptions<T>
|
||||||
|
): Promise<FindResult<T>> {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.append('class', _class)
|
||||||
|
if (query !== undefined && Object.keys(query).length > 0) {
|
||||||
|
params.append('query', JSON.stringify(query))
|
||||||
|
}
|
||||||
|
if (options !== undefined && Object.keys(options).length > 0) {
|
||||||
|
params.append('options', JSON.stringify(options))
|
||||||
|
}
|
||||||
|
const response = await fetch(concatLink(this.endpoint, `/api/v1/find-all/${this.workspace}?${params.toString()}`), {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: 'Bearer ' + this.token,
|
||||||
|
'accept-encoding': 'snappy, gzip'
|
||||||
|
},
|
||||||
|
keepalive: true
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new PlatformError(unknownError(response.statusText))
|
||||||
|
}
|
||||||
|
const encoding = response.headers.get('content-encoding')
|
||||||
|
if (encoding === 'snappy') {
|
||||||
|
const buffer = await response.arrayBuffer()
|
||||||
|
const decompressed = uncompress(buffer)
|
||||||
|
const decoder = new TextDecoder()
|
||||||
|
const jsonString = decoder.decode(decompressed)
|
||||||
|
return JSON.parse(jsonString) as FindResult<T>
|
||||||
|
}
|
||||||
|
return (await response.json()) as FindResult<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAccount (): Promise<Account> {
|
||||||
|
const response = await fetch(concatLink(this.endpoint, `/api/v1/account/${this.workspace}`), {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: 'Bearer ' + this.token
|
||||||
|
},
|
||||||
|
keepalive: true
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new PlatformError(unknownError(response.statusText))
|
||||||
|
}
|
||||||
|
return (await response.json()) as Account
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne<T extends Doc>(
|
||||||
|
_class: Ref<Class<T>>,
|
||||||
|
query: DocumentQuery<T>,
|
||||||
|
options?: FindOptions<T>
|
||||||
|
): Promise<WithLookup<T> | undefined> {
|
||||||
|
return (await this.findAll(_class, query, { ...options, limit: 1 })).shift()
|
||||||
|
}
|
||||||
|
|
||||||
|
async tx (tx: Tx): Promise<TxResult> {
|
||||||
|
const response = await fetch(concatLink(this.endpoint, `/api/v1/tx/${this.workspace}`), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: 'Bearer ' + this.token
|
||||||
|
},
|
||||||
|
keepalive: true,
|
||||||
|
body: JSON.stringify(tx)
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new PlatformError(unknownError(response.statusText))
|
||||||
|
}
|
||||||
|
return (await response.json()) as TxResult
|
||||||
|
}
|
||||||
|
}
|
@ -234,6 +234,28 @@ function toString (name: string, m: Metrics, offset: number, length: number): st
|
|||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toJson (m: Metrics): any {
|
||||||
|
const obj: any = {
|
||||||
|
$total: m.value,
|
||||||
|
$ops: m.operations
|
||||||
|
}
|
||||||
|
if (m.operations > 1) {
|
||||||
|
obj.avg = Math.round((m.value / (m.operations > 0 ? m.operations : 1)) * 100) / 100
|
||||||
|
}
|
||||||
|
if (Object.keys(m.params).length > 0) {
|
||||||
|
obj.params = m.params
|
||||||
|
}
|
||||||
|
for (const [k, v] of Object.entries(m.measurements ?? {})) {
|
||||||
|
obj[
|
||||||
|
`${k} ${v.value} ${v.operations} ${
|
||||||
|
v.operations > 1 ? Math.round((v.value / (v.operations > 0 ? m.operations : 1)) * 100) / 100 : ''
|
||||||
|
}`
|
||||||
|
] = toJson(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
@ -241,6 +263,10 @@ export function metricsToString (metrics: Metrics, name = 'System', length: numb
|
|||||||
return toString(name, metricsAggregate(metrics, 50, true), 0, length)
|
return toString(name, metricsAggregate(metrics, 50, true), 0, length)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function metricsToJson (metrics: Metrics): any {
|
||||||
|
return toJson(metricsAggregate(metrics))
|
||||||
|
}
|
||||||
|
|
||||||
function printMetricsParamsRows (
|
function printMetricsParamsRows (
|
||||||
params: Record<string, Record<string, MetricsData>>,
|
params: Record<string, Record<string, MetricsData>>,
|
||||||
offset: number
|
offset: number
|
||||||
|
@ -3,8 +3,7 @@
|
|||||||
import login from '@hcengineering/login'
|
import login from '@hcengineering/login'
|
||||||
import { getEmbeddedLabel, getMetadata } from '@hcengineering/platform'
|
import { getEmbeddedLabel, getMetadata } from '@hcengineering/platform'
|
||||||
import presentation, { getClient, isAdminUser, uiContext } from '@hcengineering/presentation'
|
import presentation, { getClient, isAdminUser, uiContext } from '@hcengineering/presentation'
|
||||||
import { Button, IconArrowLeft, IconArrowRight, fetchMetadataLocalStorage, ticker } from '@hcengineering/ui'
|
import { Button, EditBox, IconArrowLeft, IconArrowRight, fetchMetadataLocalStorage, ticker } from '@hcengineering/ui'
|
||||||
import EditBox from '@hcengineering/ui/src/components/EditBox.svelte'
|
|
||||||
import MetricsInfo from './statistics/MetricsInfo.svelte'
|
import MetricsInfo from './statistics/MetricsInfo.svelte'
|
||||||
|
|
||||||
const _endpoint: string = fetchMetadataLocalStorage(login.metadata.LoginEndpoint) ?? ''
|
const _endpoint: string = fetchMetadataLocalStorage(login.metadata.LoginEndpoint) ?? ''
|
||||||
@ -25,8 +24,6 @@
|
|||||||
|
|
||||||
let avgTime = 0
|
let avgTime = 0
|
||||||
|
|
||||||
let rps = 0
|
|
||||||
|
|
||||||
let active = 0
|
let active = 0
|
||||||
|
|
||||||
let opss = 0
|
let opss = 0
|
||||||
@ -44,7 +41,7 @@
|
|||||||
profiling = data?.profiling ?? false
|
profiling = data?.profiling ?? false
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err)
|
console.error(err, time)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
let data: any
|
let data: any
|
||||||
@ -65,13 +62,8 @@
|
|||||||
avgTime = 0
|
avgTime = 0
|
||||||
maxTime = 0
|
maxTime = 0
|
||||||
let count = commandsToSend
|
let count = commandsToSend
|
||||||
let ops = 0
|
|
||||||
avgTime = 0
|
avgTime = 0
|
||||||
opss = 0
|
opss = 0
|
||||||
const int = setInterval(() => {
|
|
||||||
rps = ops
|
|
||||||
ops = 0
|
|
||||||
}, 1000)
|
|
||||||
const rate = new RateLimiter(commandsToSendParallel)
|
const rate = new RateLimiter(commandsToSendParallel)
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
|
|
||||||
@ -96,7 +88,6 @@
|
|||||||
} else {
|
} else {
|
||||||
avgTime = ed - st
|
avgTime = ed - st
|
||||||
}
|
}
|
||||||
ops++
|
|
||||||
opss++
|
opss++
|
||||||
count--
|
count--
|
||||||
}
|
}
|
||||||
@ -112,7 +103,6 @@
|
|||||||
}
|
}
|
||||||
await rate.waitProcessing()
|
await rate.waitProcessing()
|
||||||
running = false
|
running = false
|
||||||
clearInterval(int)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadProfile (): Promise<void> {
|
async function downloadProfile (): Promise<void> {
|
||||||
@ -132,7 +122,7 @@
|
|||||||
document.body.appendChild(link)
|
document.body.appendChild(link)
|
||||||
link.click()
|
link.click()
|
||||||
document.body.removeChild(link)
|
document.body.removeChild(link)
|
||||||
void fetchStats(0)
|
await fetchStats(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
let metrics: Metrics | undefined
|
let metrics: Metrics | undefined
|
||||||
|
@ -571,6 +571,16 @@ export interface Session {
|
|||||||
) => Promise<FindResult<T>>
|
) => Promise<FindResult<T>>
|
||||||
searchFulltext: (ctx: ClientSessionCtx, query: SearchQuery, options: SearchOptions) => Promise<void>
|
searchFulltext: (ctx: ClientSessionCtx, query: SearchQuery, options: SearchOptions) => Promise<void>
|
||||||
tx: (ctx: ClientSessionCtx, tx: Tx) => Promise<void>
|
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>
|
loadChunk: (ctx: ClientSessionCtx, domain: Domain, idx?: number) => Promise<void>
|
||||||
|
|
||||||
getDomainHash: (ctx: ClientSessionCtx, domain: Domain) => Promise<void>
|
getDomainHash: (ctx: ClientSessionCtx, domain: Domain) => Promise<void>
|
||||||
@ -707,11 +717,17 @@ export interface SessionManager {
|
|||||||
createOpContext: (
|
createOpContext: (
|
||||||
ctx: MeasureContext,
|
ctx: MeasureContext,
|
||||||
pipeline: Pipeline,
|
pipeline: Pipeline,
|
||||||
request: Request<any>,
|
requestId: Request<any>['id'],
|
||||||
service: Session,
|
service: Session,
|
||||||
ws: ConnectionSocket,
|
ws: ConnectionSocket,
|
||||||
workspace: WorkspaceUuid
|
|
||||||
) => ClientSessionCtx
|
) => ClientSessionCtx
|
||||||
|
|
||||||
|
handleRPC: <S extends Session>(
|
||||||
|
requestCtx: MeasureContext,
|
||||||
|
service: S,
|
||||||
|
ws: ConnectionSocket,
|
||||||
|
operation: (ctx: ClientSessionCtx) => Promise<void>
|
||||||
|
) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -24,6 +24,8 @@ import {
|
|||||||
type FindOptions,
|
type FindOptions,
|
||||||
type FindResult,
|
type FindResult,
|
||||||
type MeasureContext,
|
type MeasureContext,
|
||||||
|
type PersonId,
|
||||||
|
type PersonUuid,
|
||||||
type Ref,
|
type Ref,
|
||||||
type SearchOptions,
|
type SearchOptions,
|
||||||
type SearchQuery,
|
type SearchQuery,
|
||||||
@ -31,9 +33,8 @@ import {
|
|||||||
type Timestamp,
|
type Timestamp,
|
||||||
type Tx,
|
type Tx,
|
||||||
type TxCUD,
|
type TxCUD,
|
||||||
type PersonId,
|
type TxResult,
|
||||||
type WorkspaceDataId,
|
type WorkspaceDataId
|
||||||
type PersonUuid
|
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import { PlatformError, unknownError } from '@hcengineering/platform'
|
import { PlatformError, unknownError } from '@hcengineering/platform'
|
||||||
import {
|
import {
|
||||||
@ -164,7 +165,14 @@ export class ClientSession implements Session {
|
|||||||
await ctx.sendResponse(ctx.requestId, await ctx.pipeline.searchFulltext(ctx.ctx, query, options))
|
await ctx.sendResponse(ctx.requestId, await ctx.pipeline.searchFulltext(ctx.ctx, query, options))
|
||||||
}
|
}
|
||||||
|
|
||||||
async tx (ctx: ClientSessionCtx, tx: Tx): Promise<void> {
|
async txRaw (
|
||||||
|
ctx: ClientSessionCtx,
|
||||||
|
tx: Tx
|
||||||
|
): Promise<{
|
||||||
|
result: TxResult
|
||||||
|
broadcastPromise: Promise<void>
|
||||||
|
asyncsPromise: Promise<void> | undefined
|
||||||
|
}> {
|
||||||
this.lastRequest = Date.now()
|
this.lastRequest = Date.now()
|
||||||
this.total.tx++
|
this.total.tx++
|
||||||
this.current.tx++
|
this.current.tx++
|
||||||
@ -173,31 +181,45 @@ export class ClientSession implements Session {
|
|||||||
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 ? ctx.pipeline.context.adapterManager?.reserveContext?.(cid) : undefined
|
||||||
|
let result: TxResult
|
||||||
try {
|
try {
|
||||||
const result = await ctx.pipeline.tx(ctx.ctx, [tx])
|
result = await ctx.pipeline.tx(ctx.ctx, [tx])
|
||||||
|
|
||||||
// Send result immideately
|
|
||||||
await ctx.sendResponse(ctx.requestId, result)
|
|
||||||
|
|
||||||
// We need to broadcast all collected transactions
|
|
||||||
await ctx.pipeline.handleBroadcast(ctx.ctx)
|
|
||||||
} finally {
|
} finally {
|
||||||
onEnd?.()
|
onEnd?.()
|
||||||
}
|
}
|
||||||
|
// Send result immideately
|
||||||
|
await ctx.sendResponse(ctx.requestId, result)
|
||||||
|
|
||||||
|
// We need to broadcast all collected transactions
|
||||||
|
const broadcastPromise = ctx.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 ?? []
|
||||||
|
let asyncsPromise: Promise<void> | undefined
|
||||||
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 ? ctx.pipeline.context.adapterManager?.reserveContext?.(cid) : undefined
|
||||||
try {
|
const handleAyncs = async (): Promise<void> => {
|
||||||
for (const r of (ctx.ctx.contextData as SessionData).asyncRequests ?? []) {
|
try {
|
||||||
await r()
|
for (const r of (ctx.ctx.contextData as SessionData).asyncRequests ?? []) {
|
||||||
|
await r()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
onEnd?.()
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
onEnd?.()
|
|
||||||
}
|
}
|
||||||
|
asyncsPromise = handleAyncs()
|
||||||
|
}
|
||||||
|
|
||||||
|
return { result, broadcastPromise, asyncsPromise }
|
||||||
|
}
|
||||||
|
|
||||||
|
async tx (ctx: ClientSessionCtx, tx: Tx): Promise<void> {
|
||||||
|
const { broadcastPromise, asyncsPromise } = await this.txRaw(ctx, tx)
|
||||||
|
await broadcastPromise
|
||||||
|
if (asyncsPromise !== undefined) {
|
||||||
|
await asyncsPromise
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,7 +91,7 @@ export interface Timeouts {
|
|||||||
reconnectTimeout: number // Default 3 seconds
|
reconnectTimeout: number // Default 3 seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
class TSessionManager implements SessionManager {
|
export class TSessionManager implements SessionManager {
|
||||||
private readonly statusPromises = new Map<string, Promise<void>>()
|
private readonly statusPromises = new Map<string, Promise<void>>()
|
||||||
readonly workspaces = new Map<WorkspaceUuid, Workspace>()
|
readonly workspaces = new Map<WorkspaceUuid, Workspace>()
|
||||||
checkInterval: any
|
checkInterval: any
|
||||||
@ -981,7 +981,7 @@ class TSessionManager implements SessionManager {
|
|||||||
createOpContext (
|
createOpContext (
|
||||||
ctx: MeasureContext,
|
ctx: MeasureContext,
|
||||||
pipeline: Pipeline,
|
pipeline: Pipeline,
|
||||||
request: Request<any>,
|
requestId: Request<any>['id'],
|
||||||
service: Session,
|
service: Session,
|
||||||
ws: ConnectionSocket,
|
ws: ConnectionSocket,
|
||||||
workspace: WorkspaceUuid
|
workspace: WorkspaceUuid
|
||||||
@ -990,7 +990,7 @@ class TSessionManager implements SessionManager {
|
|||||||
return {
|
return {
|
||||||
ctx,
|
ctx,
|
||||||
pipeline,
|
pipeline,
|
||||||
requestId: request.id,
|
requestId,
|
||||||
sendResponse: (reqId, msg) =>
|
sendResponse: (reqId, msg) =>
|
||||||
sendResponse(ctx, service, ws, {
|
sendResponse(ctx, service, ws, {
|
||||||
id: reqId,
|
id: reqId,
|
||||||
@ -1072,6 +1072,7 @@ class TSessionManager implements SessionManager {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (request.id === -2 && request.method === 'forceClose') {
|
if (request.id === -2 && request.method === 'forceClose') {
|
||||||
|
// TODO: we chould allow this only for admin or system accounts
|
||||||
let done = false
|
let done = false
|
||||||
const wsRef = this.workspaces.get(workspace)
|
const wsRef = this.workspaces.get(workspace)
|
||||||
if (wsRef?.upgrade ?? false) {
|
if (wsRef?.upgrade ?? false) {
|
||||||
@ -1106,7 +1107,7 @@ class TSessionManager implements SessionManager {
|
|||||||
const params = [...request.params]
|
const params = [...request.params]
|
||||||
|
|
||||||
await ctx.with('🧨 process', {}, (callTx) =>
|
await ctx.with('🧨 process', {}, (callTx) =>
|
||||||
f.apply(service, [this.createOpContext(callTx, pipeline, request, service, ws, workspace), ...params])
|
f.apply(service, [this.createOpContext(callTx, pipeline, request.id, service, ws), ...params])
|
||||||
)
|
)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
Analytics.handleError(err)
|
Analytics.handleError(err)
|
||||||
@ -1131,6 +1132,59 @@ class TSessionManager implements SessionManager {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleRPC<S extends Session>(
|
||||||
|
requestCtx: MeasureContext,
|
||||||
|
service: S,
|
||||||
|
ws: ConnectionSocket,
|
||||||
|
operation: (ctx: ClientSessionCtx) => Promise<void>
|
||||||
|
): Promise<void> {
|
||||||
|
const userCtx = requestCtx.newChild('📞 client', {})
|
||||||
|
|
||||||
|
// Calculate total number of clients
|
||||||
|
const reqId = generateId()
|
||||||
|
|
||||||
|
const st = Date.now()
|
||||||
|
return userCtx
|
||||||
|
.with('🧭 handleRPC', {}, async (ctx) => {
|
||||||
|
if (service.workspace.closing !== undefined) {
|
||||||
|
throw new Error('Workspace is closing')
|
||||||
|
}
|
||||||
|
|
||||||
|
service.requests.set(reqId, {
|
||||||
|
id: reqId,
|
||||||
|
params: {},
|
||||||
|
start: st
|
||||||
|
})
|
||||||
|
|
||||||
|
const pipeline =
|
||||||
|
service.workspace.pipeline instanceof Promise ? await service.workspace.pipeline : service.workspace.pipeline
|
||||||
|
|
||||||
|
try {
|
||||||
|
const uctx = this.createOpContext(ctx, pipeline, reqId, service, ws)
|
||||||
|
await operation(uctx)
|
||||||
|
} catch (err: any) {
|
||||||
|
Analytics.handleError(err)
|
||||||
|
if (LOGGING_ENABLED) {
|
||||||
|
this.ctx.error('error handle request', { error: err })
|
||||||
|
}
|
||||||
|
ws.send(
|
||||||
|
ctx,
|
||||||
|
{
|
||||||
|
id: reqId,
|
||||||
|
error: unknownError(err),
|
||||||
|
result: JSON.parse(JSON.stringify(err?.stack))
|
||||||
|
},
|
||||||
|
service.binaryMode,
|
||||||
|
service.useCompression
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
userCtx.end()
|
||||||
|
service.requests.delete(reqId)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
private async handleHello<S extends Session>(
|
private async handleHello<S extends Session>(
|
||||||
request: Request<any>,
|
request: Request<any>,
|
||||||
service: S,
|
service: S,
|
||||||
|
@ -53,6 +53,7 @@
|
|||||||
"utf-8-validate": "^6.0.4",
|
"utf-8-validate": "^6.0.4",
|
||||||
"ws": "^8.18.0",
|
"ws": "^8.18.0",
|
||||||
"body-parser": "^1.20.2",
|
"body-parser": "^1.20.2",
|
||||||
"snappy": "^7.2.2"
|
"snappy": "^7.2.2",
|
||||||
|
"@hcengineering/api-client": "^0.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
225
server/ws/src/__tests__/rest.test.ts
Normal file
225
server/ws/src/__tests__/rest.test.ts
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
//
|
||||||
|
// Copyright © 2025 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 { generateToken } from '@hcengineering/server-token'
|
||||||
|
|
||||||
|
import { createRestClient, type RestClient } from '@hcengineering/api-client'
|
||||||
|
import core, {
|
||||||
|
generateId,
|
||||||
|
getWorkspaceId,
|
||||||
|
Hierarchy,
|
||||||
|
MeasureMetricsContext,
|
||||||
|
ModelDb,
|
||||||
|
toFindResult,
|
||||||
|
type Class,
|
||||||
|
type Doc,
|
||||||
|
type DocumentQuery,
|
||||||
|
type Domain,
|
||||||
|
type FindOptions,
|
||||||
|
type FindResult,
|
||||||
|
type MeasureContext,
|
||||||
|
type Ref,
|
||||||
|
type Space,
|
||||||
|
type Tx,
|
||||||
|
type TxCreateDoc,
|
||||||
|
type TxResult
|
||||||
|
} from '@hcengineering/core'
|
||||||
|
import { ClientSession, startSessionManager, type TSessionManager } from '@hcengineering/server'
|
||||||
|
import { createDummyStorageAdapter, type SessionManager, type WorkspaceLoginInfo } from '@hcengineering/server-core'
|
||||||
|
import { startHttpServer } from '../server_http'
|
||||||
|
import { genMinModel } from './minmodel'
|
||||||
|
|
||||||
|
describe('rest-server', () => {
|
||||||
|
async function getModelDb (): Promise<{ modelDb: ModelDb, hierarchy: Hierarchy, txes: Tx[] }> {
|
||||||
|
const txes = genMinModel()
|
||||||
|
const hierarchy = new Hierarchy()
|
||||||
|
for (const tx of txes) {
|
||||||
|
hierarchy.tx(tx)
|
||||||
|
}
|
||||||
|
const modelDb = new ModelDb(hierarchy)
|
||||||
|
for (const tx of txes) {
|
||||||
|
await modelDb.tx(tx)
|
||||||
|
}
|
||||||
|
return { modelDb, hierarchy, txes }
|
||||||
|
}
|
||||||
|
|
||||||
|
let shutdown: () => Promise<void>
|
||||||
|
let sessionManager: SessionManager
|
||||||
|
const port: number = 3330
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
;({ shutdown, sessionManager } = startSessionManager(new MeasureMetricsContext('test', {}), {
|
||||||
|
pipelineFactory: async () => {
|
||||||
|
const { modelDb, hierarchy, txes } = await getModelDb()
|
||||||
|
return {
|
||||||
|
hierarchy,
|
||||||
|
modelDb,
|
||||||
|
context: {
|
||||||
|
workspace: {
|
||||||
|
name: 'test-ws',
|
||||||
|
workspaceName: 'test-ws',
|
||||||
|
workspaceUrl: 'test-ws'
|
||||||
|
},
|
||||||
|
hierarchy,
|
||||||
|
modelDb,
|
||||||
|
lastTx: generateId(),
|
||||||
|
lastHash: generateId(),
|
||||||
|
contextVars: {},
|
||||||
|
branding: null
|
||||||
|
},
|
||||||
|
handleBroadcast: async (ctx) => {},
|
||||||
|
findAll: async <T extends Doc>(
|
||||||
|
ctx: MeasureContext,
|
||||||
|
_class: Ref<Class<T>>,
|
||||||
|
query: DocumentQuery<T>,
|
||||||
|
options?: FindOptions<T>
|
||||||
|
): Promise<FindResult<T>> => toFindResult(await modelDb.findAll(_class, query, options)),
|
||||||
|
tx: async (ctx: MeasureContext, tx: Tx[]): Promise<[TxResult, Tx[], string[] | undefined]> => [
|
||||||
|
await modelDb.tx(...tx),
|
||||||
|
[],
|
||||||
|
undefined
|
||||||
|
],
|
||||||
|
close: async () => {},
|
||||||
|
domains: async () => hierarchy.domains(),
|
||||||
|
groupBy: async () => new Map(),
|
||||||
|
find: (ctx: MeasureContext, domain: Domain) => ({
|
||||||
|
next: async (ctx: MeasureContext) => undefined,
|
||||||
|
close: async (ctx: MeasureContext) => {}
|
||||||
|
}),
|
||||||
|
load: async (ctx: MeasureContext, domain: Domain, docs: Ref<Doc>[]) => [],
|
||||||
|
upload: async (ctx: MeasureContext, domain: Domain, docs: Doc[]) => {},
|
||||||
|
clean: async (ctx: MeasureContext, domain: Domain, docs: Ref<Doc>[]) => {},
|
||||||
|
searchFulltext: async (ctx, query, options) => {
|
||||||
|
return { docs: [] }
|
||||||
|
},
|
||||||
|
loadModel: async (ctx, lastModelTx, hash) => ({
|
||||||
|
full: true,
|
||||||
|
hash: generateId(),
|
||||||
|
transactions: txes
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sessionFactory: (token, workspace) => new ClientSession(token, workspace, true),
|
||||||
|
port,
|
||||||
|
brandingMap: {},
|
||||||
|
serverFactory: startHttpServer,
|
||||||
|
accountsUrl: '',
|
||||||
|
externalStorage: createDummyStorageAdapter()
|
||||||
|
}))
|
||||||
|
jest
|
||||||
|
.spyOn(sessionManager as TSessionManager, 'getWorkspaceInfo')
|
||||||
|
.mockImplementation(async (ctx: MeasureContext, token: string): Promise<WorkspaceLoginInfo> => {
|
||||||
|
return {
|
||||||
|
workspaceId: 'test-ws',
|
||||||
|
workspaceUrl: 'test-ws',
|
||||||
|
workspaceName: 'Test Workspace',
|
||||||
|
uuid: 'test-ws',
|
||||||
|
createdBy: 'test-owner',
|
||||||
|
mode: 'active',
|
||||||
|
createdOn: Date.now(),
|
||||||
|
lastVisit: Date.now(),
|
||||||
|
disabled: false,
|
||||||
|
endpoint: `http://localhost:${port}`,
|
||||||
|
region: 'test-region',
|
||||||
|
targetRegion: 'test-region',
|
||||||
|
backupInfo: {
|
||||||
|
dataSize: 0,
|
||||||
|
blobsSize: 0,
|
||||||
|
backupSize: 0,
|
||||||
|
lastBackup: 0,
|
||||||
|
backups: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
afterAll(async () => {
|
||||||
|
await shutdown()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function connect (): Promise<RestClient> {
|
||||||
|
const token: string = generateToken('user1@site.com', getWorkspaceId('test-ws'))
|
||||||
|
return await createRestClient(`http://localhost:${port}`, 'test-ws', token)
|
||||||
|
}
|
||||||
|
|
||||||
|
it('get account', async () => {
|
||||||
|
const conn = await connect()
|
||||||
|
const account = await conn.getAccount()
|
||||||
|
|
||||||
|
expect(account.email).toBe('user1@site.com')
|
||||||
|
expect(account.role).toBe('OWNER')
|
||||||
|
expect(account._id).toBe('User1')
|
||||||
|
expect(account._class).toBe('core:class:Account')
|
||||||
|
expect(account.space).toBe('core:space:Model')
|
||||||
|
expect(account.modifiedBy).toBe('core:account:System')
|
||||||
|
expect(account.createdBy).toBe('core:account:System')
|
||||||
|
expect(typeof account.modifiedOn).toBe('number')
|
||||||
|
expect(typeof account.createdOn).toBe('number')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('find spaces', async () => {
|
||||||
|
const conn = await connect()
|
||||||
|
const spaces = await conn.findAll(core.class.Space, {})
|
||||||
|
expect(spaces.length).toBe(2)
|
||||||
|
expect(spaces[0].name).toBe('Sp1')
|
||||||
|
expect(spaces[1].name).toBe('Sp2')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('find avg', async () => {
|
||||||
|
const conn = await connect()
|
||||||
|
let ops = 0
|
||||||
|
let total = 0
|
||||||
|
const attempts = 1000
|
||||||
|
for (let i = 0; i < attempts; i++) {
|
||||||
|
const st = performance.now()
|
||||||
|
const spaces = await conn.findAll(core.class.Space, {})
|
||||||
|
expect(spaces.length).toBe(2)
|
||||||
|
expect(spaces[0].name).toBe('Sp1')
|
||||||
|
expect(spaces[1].name).toBe('Sp2')
|
||||||
|
const ed = performance.now()
|
||||||
|
ops++
|
||||||
|
total += ed - st
|
||||||
|
}
|
||||||
|
const avg = total / ops
|
||||||
|
// console.log('ops:', ops, 'total:', total, 'avg:', )
|
||||||
|
expect(ops).toEqual(attempts)
|
||||||
|
expect(avg).toBeLessThan(5) // 5ms max per operation
|
||||||
|
})
|
||||||
|
|
||||||
|
it('add space', async () => {
|
||||||
|
const conn = await connect()
|
||||||
|
const account = await conn.getAccount()
|
||||||
|
const tx: TxCreateDoc<Space> = {
|
||||||
|
_class: core.class.TxCreateDoc,
|
||||||
|
space: core.space.Tx,
|
||||||
|
_id: generateId(),
|
||||||
|
objectSpace: core.space.Model,
|
||||||
|
modifiedBy: account._id,
|
||||||
|
modifiedOn: Date.now(),
|
||||||
|
attributes: {
|
||||||
|
name: 'Sp3',
|
||||||
|
description: '',
|
||||||
|
private: false,
|
||||||
|
archived: false,
|
||||||
|
members: [],
|
||||||
|
autoJoin: false
|
||||||
|
},
|
||||||
|
objectClass: core.class.Space,
|
||||||
|
objectId: generateId()
|
||||||
|
}
|
||||||
|
await conn.tx(tx)
|
||||||
|
const spaces = await conn.findAll(core.class.Space, {})
|
||||||
|
expect(spaces.length).toBe(3)
|
||||||
|
})
|
||||||
|
})
|
182
server/ws/src/rpc.ts
Normal file
182
server/ws/src/rpc.ts
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
import type { Class, Doc, MeasureContext, Ref } from '@hcengineering/core'
|
||||||
|
import type {
|
||||||
|
ClientSessionCtx,
|
||||||
|
ConnectionSocket,
|
||||||
|
PipelineFactory,
|
||||||
|
Session,
|
||||||
|
SessionManager
|
||||||
|
} from '@hcengineering/server-core'
|
||||||
|
import { decodeToken } from '@hcengineering/server-token'
|
||||||
|
|
||||||
|
import { type Express, type Response as ExpressResponse, type Request } from 'express'
|
||||||
|
import type { OutgoingHttpHeaders } from 'http2'
|
||||||
|
import { compress } from 'snappy'
|
||||||
|
import { promisify } from 'util'
|
||||||
|
import { gzip } from 'zlib'
|
||||||
|
import { retrieveJson } from './utils'
|
||||||
|
interface RPCClientInfo {
|
||||||
|
client: ConnectionSocket
|
||||||
|
session: Session
|
||||||
|
workspaceId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const gzipAsync = promisify(gzip)
|
||||||
|
|
||||||
|
const sendError = (res: ExpressResponse, code: number, data: any): void => {
|
||||||
|
res.writeHead(code, {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
'keep-alive': 'timeout=5, max=1000'
|
||||||
|
})
|
||||||
|
res.end(JSON.stringify(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendJson (req: Request, res: ExpressResponse, result: any): Promise<void> {
|
||||||
|
const headers: OutgoingHttpHeaders = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
'keep-alive': 'timeout=5, max=1000'
|
||||||
|
}
|
||||||
|
let body: any = JSON.stringify(result)
|
||||||
|
|
||||||
|
const contentEncodings: string[] =
|
||||||
|
typeof req.headers['accept-encoding'] === 'string'
|
||||||
|
? req.headers['accept-encoding'].split(',').map((it) => it.trim())
|
||||||
|
: req.headers['accept-encoding'] ?? []
|
||||||
|
for (const contentEncoding of contentEncodings) {
|
||||||
|
let done = false
|
||||||
|
switch (contentEncoding) {
|
||||||
|
case 'snappy':
|
||||||
|
headers['content-encoding'] = 'snappy'
|
||||||
|
body = await compress(body)
|
||||||
|
done = true
|
||||||
|
break
|
||||||
|
case 'gzip':
|
||||||
|
headers['content-encoding'] = 'gzip'
|
||||||
|
body = await gzipAsync(body)
|
||||||
|
done = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (done) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(200, headers)
|
||||||
|
res.end(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerRPC (
|
||||||
|
app: Express,
|
||||||
|
sessions: SessionManager,
|
||||||
|
ctx: MeasureContext,
|
||||||
|
pipelineFactory: PipelineFactory
|
||||||
|
): void {
|
||||||
|
const rpcSessions = new Map<string, RPCClientInfo>()
|
||||||
|
|
||||||
|
async function withSession (
|
||||||
|
req: Request,
|
||||||
|
res: ExpressResponse,
|
||||||
|
operation: (ctx: ClientSessionCtx, session: Session) => Promise<void>
|
||||||
|
): Promise<void> {
|
||||||
|
if (req.params.workspaceId === undefined || req.params.workspaceId === '') {
|
||||||
|
res.writeHead(400, {})
|
||||||
|
res.end('Missing workspace')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let token = req.headers.authorization as string
|
||||||
|
if (token === null) {
|
||||||
|
sendError(res, 401, { message: 'Missing Authorization header' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const workspaceId = decodeURIComponent(req.params.workspaceId)
|
||||||
|
token = token.split(' ')[1]
|
||||||
|
|
||||||
|
const decodedToken = decodeToken(token)
|
||||||
|
if (workspaceId !== decodedToken.workspace.name) {
|
||||||
|
sendError(res, 401, { message: 'Invalid workspace' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let transactorRpc = rpcSessions.get(token)
|
||||||
|
|
||||||
|
if (transactorRpc === undefined) {
|
||||||
|
const cs: ConnectionSocket = createClosingSocket(token, rpcSessions)
|
||||||
|
const s = await sessions.addSession(ctx, cs, decodedToken, token, pipelineFactory, token)
|
||||||
|
if (!('session' in s)) {
|
||||||
|
sendError(res, 401, {
|
||||||
|
message: 'Failed to create session',
|
||||||
|
mode: 'specialError' in s ? s.specialError ?? '' : 'upgrading'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
transactorRpc = { session: s.session, client: cs, workspaceId: s.workspaceId }
|
||||||
|
rpcSessions.set(token, transactorRpc)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const rpc = transactorRpc
|
||||||
|
await sessions.handleRPC(ctx, rpc.session, rpc.client, async (ctx) => {
|
||||||
|
await operation(ctx, rpc.session)
|
||||||
|
})
|
||||||
|
} catch (err: any) {
|
||||||
|
sendError(res, 401, { message: 'Failed to execute operation', error: err.message, stack: err.stack })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get('/api/v1/ping/:workspaceId', (req, res) => {
|
||||||
|
void withSession(req, res, async (ctx, session) => {
|
||||||
|
await session.ping(ctx)
|
||||||
|
await sendJson(req, res, { pong: true })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/api/v1/find-all/:workspaceId', (req, res) => {
|
||||||
|
void withSession(req, res, async (ctx, session) => {
|
||||||
|
const _class = req.query.class as Ref<Class<Doc>>
|
||||||
|
const query = req.query.query !== undefined ? JSON.parse(req.query.query as string) : {}
|
||||||
|
const options = req.query.options !== undefined ? JSON.parse(req.query.options as string) : {}
|
||||||
|
|
||||||
|
const result = await session.findAllRaw(ctx.ctx, ctx.pipeline, _class, query, options)
|
||||||
|
await sendJson(req, res, result)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post('/api/v1/find-all/:workspaceId', (req, res) => {
|
||||||
|
void withSession(req, res, async (ctx, session) => {
|
||||||
|
const { _class, query, options }: any = (await retrieveJson(req)) ?? {}
|
||||||
|
|
||||||
|
const result = await session.findAllRaw(ctx.ctx, ctx.pipeline, _class, query, options)
|
||||||
|
await sendJson(req, res, result)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post('/api/v1/tx/:workspaceId', (req, res) => {
|
||||||
|
void withSession(req, res, async (ctx, session) => {
|
||||||
|
const tx: any = (await retrieveJson(req)) ?? {}
|
||||||
|
|
||||||
|
const result = await session.txRaw(ctx, tx)
|
||||||
|
await sendJson(req, res, result.result)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
app.get('/api/v1/account/:workspaceId', (req, res) => {
|
||||||
|
void withSession(req, res, async (ctx, session) => {
|
||||||
|
const result = session.getRawAccount(ctx.pipeline)
|
||||||
|
await sendJson(req, res, result)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function createClosingSocket (rawToken: string, rpcSessions: Map<string, RPCClientInfo>): ConnectionSocket {
|
||||||
|
return {
|
||||||
|
id: rawToken,
|
||||||
|
isClosed: false,
|
||||||
|
close: () => {
|
||||||
|
rpcSessions.delete(rawToken)
|
||||||
|
},
|
||||||
|
send: (ctx, msg, binary, compression) => {},
|
||||||
|
sendPong: () => {},
|
||||||
|
data: () => ({}),
|
||||||
|
readRequest: (buffer, binary) => ({ method: '', params: [], id: -1, time: Date.now() }),
|
||||||
|
checkState: () => true
|
||||||
|
}
|
||||||
|
}
|
@ -59,6 +59,8 @@ import { WebSocketServer, type RawData, type WebSocket } from 'ws'
|
|||||||
import 'bufferutil'
|
import 'bufferutil'
|
||||||
import { compress } from 'snappy'
|
import { compress } from 'snappy'
|
||||||
import 'utf-8-validate'
|
import 'utf-8-validate'
|
||||||
|
import { registerRPC } from './rpc'
|
||||||
|
import { retrieveJson } from './utils'
|
||||||
|
|
||||||
let profiling = false
|
let profiling = false
|
||||||
const rpcHandler = new RPCHandler()
|
const rpcHandler = new RPCHandler()
|
||||||
@ -151,6 +153,7 @@ export function startHttpServer (
|
|||||||
res.end()
|
res.end()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
app.get('/api/v1/profiling', (req, res) => {
|
app.get('/api/v1/profiling', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const token = req.query.token as string
|
const token = req.query.token as string
|
||||||
@ -357,6 +360,8 @@ export function startHttpServer (
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
registerRPC(app, sessions, ctx, pipelineFactory)
|
||||||
|
|
||||||
app.put('/api/v1/broadcast', (req, res) => {
|
app.put('/api/v1/broadcast', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const token = req.query.token as string
|
const token = req.query.token as string
|
||||||
@ -364,26 +369,19 @@ export function startHttpServer (
|
|||||||
const ws = sessions.workspaces.get(req.query.workspace as WorkspaceUuid)
|
const ws = sessions.workspaces.get(req.query.workspace as WorkspaceUuid)
|
||||||
if (ws !== undefined) {
|
if (ws !== undefined) {
|
||||||
// push the data to body
|
// push the data to body
|
||||||
const body: Buffer[] = []
|
void retrieveJson(req)
|
||||||
req
|
.then((data) => {
|
||||||
.on('data', (chunk) => {
|
if (Array.isArray(data)) {
|
||||||
body.push(chunk)
|
sessions.broadcastAll(ws, data as Tx[])
|
||||||
})
|
} else {
|
||||||
.on('end', () => {
|
sessions.broadcastAll(ws, [data as unknown as Tx])
|
||||||
// on end of data, perform necessary action
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(Buffer.concat(body as any).toString())
|
|
||||||
if (Array.isArray(data)) {
|
|
||||||
sessions.broadcastAll(ws, data as Tx[])
|
|
||||||
} else {
|
|
||||||
sessions.broadcastAll(ws, [data as unknown as Tx])
|
|
||||||
}
|
|
||||||
res.end()
|
|
||||||
} catch (err: any) {
|
|
||||||
ctx.error('JSON parse error', { err })
|
|
||||||
res.writeHead(400, {})
|
|
||||||
res.end()
|
|
||||||
}
|
}
|
||||||
|
res.end()
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
ctx.error('JSON parse error', { err })
|
||||||
|
res.writeHead(400, {})
|
||||||
|
res.end()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
res.writeHead(404, {})
|
res.writeHead(404, {})
|
||||||
|
23
server/ws/src/utils.ts
Normal file
23
server/ws/src/utils.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import type { Request } from 'express'
|
||||||
|
|
||||||
|
export function retrieveJson (req: Request): Promise<any> {
|
||||||
|
const body: Uint8Array[] = []
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
req
|
||||||
|
.on('data', (chunk: Uint8Array) => {
|
||||||
|
body.push(chunk)
|
||||||
|
})
|
||||||
|
.on('error', (err) => {
|
||||||
|
reject(err)
|
||||||
|
})
|
||||||
|
.on('end', () => {
|
||||||
|
// on end of data, perform necessary action
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(Buffer.concat(body).toString())
|
||||||
|
resolve(data)
|
||||||
|
} catch (err: any) {
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user