Initial rest RPC (#8076)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev
2025-02-24 12:40:56 +07:00
committed by Andrey Sobolev
parent 0d76e68516
commit cd90f8bce6
13 changed files with 727 additions and 57 deletions
+38 -16
View File
@@ -24,6 +24,8 @@ import {
type FindOptions,
type FindResult,
type MeasureContext,
type PersonId,
type PersonUuid,
type Ref,
type SearchOptions,
type SearchQuery,
@@ -31,9 +33,8 @@ import {
type Timestamp,
type Tx,
type TxCUD,
type PersonId,
type WorkspaceDataId,
type PersonUuid
type TxResult,
type WorkspaceDataId
} from '@hcengineering/core'
import { PlatformError, unknownError } from '@hcengineering/platform'
import {
@@ -164,7 +165,14 @@ export class ClientSession implements Session {
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.total.tx++
this.current.tx++
@@ -173,31 +181,45 @@ export class ClientSession implements Session {
let cid = 'client_' + generateId()
ctx.ctx.id = cid
let onEnd = useReserveContext ? ctx.pipeline.context.adapterManager?.reserveContext?.(cid) : undefined
let result: TxResult
try {
const 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)
result = await ctx.pipeline.tx(ctx.ctx, [tx])
} finally {
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
const asyncs = (ctx.ctx.contextData as SessionData).asyncRequests ?? []
let asyncsPromise: Promise<void> | undefined
if (asyncs.length > 0) {
cid = 'client_async_' + generateId()
ctx.ctx.id = cid
onEnd = useReserveContext ? ctx.pipeline.context.adapterManager?.reserveContext?.(cid) : undefined
try {
for (const r of (ctx.ctx.contextData as SessionData).asyncRequests ?? []) {
await r()
const handleAyncs = async (): Promise<void> => {
try {
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
}
}
+58 -4
View File
@@ -91,7 +91,7 @@ export interface Timeouts {
reconnectTimeout: number // Default 3 seconds
}
class TSessionManager implements SessionManager {
export class TSessionManager implements SessionManager {
private readonly statusPromises = new Map<string, Promise<void>>()
readonly workspaces = new Map<WorkspaceUuid, Workspace>()
checkInterval: any
@@ -981,7 +981,7 @@ class TSessionManager implements SessionManager {
createOpContext (
ctx: MeasureContext,
pipeline: Pipeline,
request: Request<any>,
requestId: Request<any>['id'],
service: Session,
ws: ConnectionSocket,
workspace: WorkspaceUuid
@@ -990,7 +990,7 @@ class TSessionManager implements SessionManager {
return {
ctx,
pipeline,
requestId: request.id,
requestId,
sendResponse: (reqId, msg) =>
sendResponse(ctx, service, ws, {
id: reqId,
@@ -1072,6 +1072,7 @@ class TSessionManager implements SessionManager {
return
}
if (request.id === -2 && request.method === 'forceClose') {
// TODO: we chould allow this only for admin or system accounts
let done = false
const wsRef = this.workspaces.get(workspace)
if (wsRef?.upgrade ?? false) {
@@ -1106,7 +1107,7 @@ class TSessionManager implements SessionManager {
const params = [...request.params]
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) {
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>(
request: Request<any>,
service: S,