diff --git a/src/adapters/rpc-server/constants.ts b/src/adapters/rpc-server/constants.ts new file mode 100644 index 0000000..68d5735 --- /dev/null +++ b/src/adapters/rpc-server/constants.ts @@ -0,0 +1,2 @@ +export const FRIENDSHIPS_COUNT_PAGE_STREAM = 20 +export const INTERNAL_SERVER_ERROR = 'SERVER ERROR' diff --git a/src/adapters/rpc-server/index.ts b/src/adapters/rpc-server/index.ts new file mode 100644 index 0000000..4440b96 --- /dev/null +++ b/src/adapters/rpc-server/index.ts @@ -0,0 +1 @@ +export * from './rpc-server' diff --git a/src/adapters/rpc-server/rpc-server.ts b/src/adapters/rpc-server/rpc-server.ts new file mode 100644 index 0000000..cd7210c --- /dev/null +++ b/src/adapters/rpc-server/rpc-server.ts @@ -0,0 +1,78 @@ +import { Transport, createRpcServer } from '@dcl/rpc' +import { SocialServiceDefinition } from '@dcl/protocol/out-js/decentraland/social_service_v2/social_service.gen' +import { registerService } from '@dcl/rpc/dist/codegen' +import { IBaseComponent } from '@well-known-components/interfaces' +import { AppComponents, RpcServerContext, SubscriptionEventsEmitter } from '../../types' +import { getFriendsService } from './services/get-friends' +import { getMutualFriendsService } from './services/get-mutual-friends' +import { getPendingFriendshipRequestsService } from './services/get-pending-friendship-requests' +import { upsertFriendshipService } from './services/upsert-friendship' +import { subscribeToFriendshipUpdatesService } from './services/subscribe-to-friendship-updates' + +export type IRPCServerComponent = IBaseComponent & { + attachUser(user: { transport: Transport; address: string }): void +} + +export async function createRpcServerComponent( + components: Pick +): Promise { + const { logs, db, pubsub, config, server } = components + + const SHARED_CONTEXT: Pick = { + subscribers: {} + } + + const rpcServer = createRpcServer({ + logger: logs.getLogger('rpcServer') + }) + + const logger = logs.getLogger('rpcServer-handler') + + const rpcServerPort = (await config.getNumber('RPC_SERVER_PORT')) || 8085 + + const getFriends = getFriendsService({ components: { logs, db } }) + const getMutualFriends = getMutualFriendsService({ components: { logs, db } }) + const getPendingFriendshipRequests = getPendingFriendshipRequestsService({ components: { logs, db } }) + const getSentFriendshipRequests = getPendingFriendshipRequestsService({ components: { logs, db } }) + const upsertFriendship = upsertFriendshipService({ components: { logs, db, pubsub } }) + const subscribeToFriendshipUpdates = subscribeToFriendshipUpdatesService({ components: { logs } }) + + rpcServer.setHandler(async function handler(port) { + registerService(port, SocialServiceDefinition, async () => ({ + getFriends, + getMutualFriends, + getPendingFriendshipRequests, + getSentFriendshipRequests, + upsertFriendship, + subscribeToFriendshipUpdates + })) + }) + + return { + async start() { + server.app.listen(rpcServerPort, () => { + logger.info(`[RPC] RPC Server listening on port ${rpcServerPort}`) + }) + + await pubsub.subscribeToFriendshipUpdates((message) => { + try { + const update = JSON.parse(message) as SubscriptionEventsEmitter['update'] + const updateEmitter = SHARED_CONTEXT.subscribers[update.to] + if (updateEmitter) { + updateEmitter.emit('update', update) + } + } catch (error) { + logger.error(error as any) + } + }) + }, + attachUser({ transport, address }) { + transport.on('close', () => { + if (SHARED_CONTEXT.subscribers[address]) { + delete SHARED_CONTEXT.subscribers[address] + } + }) + rpcServer.attachTransport(transport, { subscribers: SHARED_CONTEXT.subscribers, address }) + } + } +} diff --git a/src/adapters/rpc-server/services/get-friends.ts b/src/adapters/rpc-server/services/get-friends.ts new file mode 100644 index 0000000..5a9946d --- /dev/null +++ b/src/adapters/rpc-server/services/get-friends.ts @@ -0,0 +1,50 @@ +import { Empty } from '@dcl/protocol/out-ts/google/protobuf/empty.gen' +import { Friendship, RpcServerContext, RPCServiceContext } from '../../../types' +import { INTERNAL_SERVER_ERROR, FRIENDSHIPS_COUNT_PAGE_STREAM } from '../constants' +import { UsersResponse } from '@dcl/protocol/out-ts/decentraland/social_service_v2/social_service.gen' + +export function getFriendsService({ components: { logs, db } }: RPCServiceContext<'logs' | 'db'>) { + const logger = logs.getLogger('get-friends-service') + + return async function* (_request: Empty, context: RpcServerContext): AsyncGenerator { + logger.debug('getting friends for ', { address: context.address }) + let friendsGenerator: AsyncGenerator | undefined + try { + friendsGenerator = db.getFriends(context.address) + } catch (error) { + logger.error(error as any) + // throw an error bc there is no sense to create a generator to send an error + // as it's done in the previous Social Service + throw new Error(INTERNAL_SERVER_ERROR) + } + + const generator = async function* () { + let users = [] + for await (const friendship of friendsGenerator) { + const { address_requested, address_requester } = friendship + if (context.address === address_requested) { + users.push({ address: address_requester }) + } else { + users.push({ address: address_requested }) + } + + if (users.length === FRIENDSHIPS_COUNT_PAGE_STREAM) { + const response = { + users: [...users] + } + users = [] + yield response + } + } + + if (users.length) { + const response = { + users + } + yield response + } + } + + return generator() + } +} diff --git a/src/adapters/rpc-server/services/get-mutual-friends.ts b/src/adapters/rpc-server/services/get-mutual-friends.ts new file mode 100644 index 0000000..33e09f1 --- /dev/null +++ b/src/adapters/rpc-server/services/get-mutual-friends.ts @@ -0,0 +1,47 @@ +import { RpcServerContext, RPCServiceContext } from '../../../types' +import { INTERNAL_SERVER_ERROR, FRIENDSHIPS_COUNT_PAGE_STREAM } from '../constants' +import { + MutualFriendsPayload, + UsersResponse +} from '@dcl/protocol/out-ts/decentraland/social_service_v2/social_service.gen' +import { normalizeAddress } from '../../../utils/address' + +export function getMutualFriendsService({ components: { logs, db } }: RPCServiceContext<'logs' | 'db'>) { + const logger = logs.getLogger('get-mutual-friends-service') + + return async function* (request: MutualFriendsPayload, context: RpcServerContext): AsyncGenerator { + logger.debug(`getting mutual friends ${context.address}<>${request.user!.address}`) + let mutualFriends: AsyncGenerator<{ address: string }> | undefined + try { + mutualFriends = db.getMutualFriends(context.address, normalizeAddress(request.user!.address)) + } catch (error) { + logger.error(error as any) + // throw an error bc there is no sense to create a generator to send an error + // as it's done in the previous Social Service + throw new Error(INTERNAL_SERVER_ERROR) + } + + const generator = async function* () { + const users = [] + for await (const friendship of mutualFriends) { + const { address } = friendship + users.push({ address }) + if (users.length === FRIENDSHIPS_COUNT_PAGE_STREAM) { + const response = { + users + } + yield response + } + } + + if (users.length) { + const response = { + users + } + yield response + } + } + + return generator() + } +} diff --git a/src/adapters/rpc-server/services/get-pending-friendship-requests.ts b/src/adapters/rpc-server/services/get-pending-friendship-requests.ts new file mode 100644 index 0000000..c7775d1 --- /dev/null +++ b/src/adapters/rpc-server/services/get-pending-friendship-requests.ts @@ -0,0 +1,35 @@ +import { Empty } from '@dcl/protocol/out-ts/google/protobuf/empty.gen' +import { RpcServerContext, RPCServiceContext } from '../../../types' +import { FriendshipRequestsResponse } from '@dcl/protocol/out-ts/decentraland/social_service_v2/social_service.gen' + +export function getPendingFriendshipRequestsService({ components: { logs, db } }: RPCServiceContext<'logs' | 'db'>) { + const logger = logs.getLogger('get-pending-friendship-requests-service') + + return async function (_request: Empty, context: RpcServerContext): Promise { + try { + const pendingRequests = await db.getReceivedFriendshipRequests(context.address) + const mappedRequests = pendingRequests.map(({ address, timestamp, metadata }) => ({ + user: { address }, + createdAt: new Date(timestamp).getTime(), + message: metadata?.message || '' + })) + + return { + response: { + $case: 'requests', + requests: { + requests: mappedRequests + } + } + } + } catch (error) { + logger.error(error as any) + return { + response: { + $case: 'internalServerError', + internalServerError: {} + } + } + } + } +} diff --git a/src/adapters/rpc-server/services/get-sent-friendship-requests.ts b/src/adapters/rpc-server/services/get-sent-friendship-requests.ts new file mode 100644 index 0000000..e1a9b22 --- /dev/null +++ b/src/adapters/rpc-server/services/get-sent-friendship-requests.ts @@ -0,0 +1,35 @@ +import { Empty } from '@dcl/protocol/out-ts/google/protobuf/empty.gen' +import { RpcServerContext, RPCServiceContext } from '../../../types' +import { FriendshipRequestsResponse } from '@dcl/protocol/out-ts/decentraland/social_service_v2/social_service.gen' + +export function getSentFriendshipRequestsService({ components: { logs, db } }: RPCServiceContext<'logs' | 'db'>) { + const logger = logs.getLogger('get-sent-friendship-requests-service') + + return async function (_request: Empty, context: RpcServerContext): Promise { + try { + const pendingRequests = await db.getSentFriendshipRequests(context.address) + const mappedRequests = pendingRequests.map(({ address, timestamp, metadata }) => ({ + user: { address }, + createdAt: new Date(timestamp).getTime(), + message: metadata?.message || '' + })) + + return { + response: { + $case: 'requests', + requests: { + requests: mappedRequests + } + } + } + } catch (error) { + logger.error(error as any) + return { + response: { + $case: 'internalServerError', + internalServerError: {} + } + } + } + } +} diff --git a/src/adapters/rpc-server/services/subscribe-to-friendship-updates.ts b/src/adapters/rpc-server/services/subscribe-to-friendship-updates.ts new file mode 100644 index 0000000..a9b3a84 --- /dev/null +++ b/src/adapters/rpc-server/services/subscribe-to-friendship-updates.ts @@ -0,0 +1,30 @@ +import { Empty } from '@dcl/protocol/out-ts/google/protobuf/empty.gen' +import { RpcServerContext, RPCServiceContext, SubscriptionEventsEmitter } from '../../../types' +import { FriendshipUpdate } from '@dcl/protocol/out-ts/decentraland/social_service_v2/social_service.gen' +import mitt from 'mitt' +import { parseEmittedUpdateToFriendshipUpdate } from '../../../logic/friendships' +import emitterToAsyncGenerator from '../../../utils/emitterToGenerator' + +export function subscribeToFriendshipUpdatesService({ components: { logs } }: RPCServiceContext<'logs'>) { + const logger = logs.getLogger('subscribe-to-friendship-updates-service') + + return async function* (_request: Empty, context: RpcServerContext): AsyncGenerator { + const eventEmitter = mitt() + context.subscribers[context.address] = eventEmitter + const updatesGenerator = emitterToAsyncGenerator(eventEmitter, 'update') + + const generator = async function* () { + for await (const update of updatesGenerator) { + logger.debug('> friendship update received, sending: ', { update: update as any }) + const updateToResponse = parseEmittedUpdateToFriendshipUpdate(update) + if (updateToResponse) { + yield updateToResponse + } else { + logger.error('> unable to parse update to FriendshipUpdate > ', { update: update as any }) + } + } + } + + return generator() + } +} diff --git a/src/adapters/rpc-server/services/upsert-friendship.ts b/src/adapters/rpc-server/services/upsert-friendship.ts new file mode 100644 index 0000000..2c163e4 --- /dev/null +++ b/src/adapters/rpc-server/services/upsert-friendship.ts @@ -0,0 +1,114 @@ +import { Action, FriendshipStatus, RpcServerContext, RPCServiceContext } from '../../../types' +import { + UpsertFriendshipPayload, + UpsertFriendshipResponse +} from '@dcl/protocol/out-ts/decentraland/social_service_v2/social_service.gen' +import { + parseUpsertFriendshipRequest, + validateNewFriendshipAction, + getNewFriendshipStatus +} from '../../../logic/friendships' + +export function upsertFriendshipService({ + components: { logs, db, pubsub } +}: RPCServiceContext<'logs' | 'db' | 'pubsub'>) { + const logger = logs.getLogger('upsert-friendship-service') + + return async function ( + request: UpsertFriendshipPayload, + context: RpcServerContext + ): Promise { + const parsedRequest = parseUpsertFriendshipRequest(request) + if (!parsedRequest) { + logger.error('upsert friendship received unknown message: ', request as any) + return { + response: { + $case: 'internalServerError', + internalServerError: {} + } + } + } + + logger.debug(`upsert friendship > `, parsedRequest as Record) + + try { + const friendship = await db.getFriendship([context.address, parsedRequest.user!]) + let lastAction = undefined + if (friendship) { + const lastRecordedAction = await db.getLastFriendshipAction(friendship.id) + lastAction = lastRecordedAction + } + + if ( + !validateNewFriendshipAction( + context.address, + { action: parsedRequest.action, user: parsedRequest.user }, + lastAction + ) + ) { + logger.error('invalid action for a friendship') + return { + response: { + $case: 'invalidFriendshipAction', + invalidFriendshipAction: {} + } + } + } + + const friendshipStatus = getNewFriendshipStatus(parsedRequest.action) + const isActive = friendshipStatus === FriendshipStatus.Friends + + logger.debug('friendship status > ', { isActive: JSON.stringify(isActive), friendshipStatus }) + + const id = await db.executeTx(async (tx) => { + let id + if (friendship) { + await db.updateFriendshipStatus(friendship.id, isActive, tx) + id = friendship.id + } else { + const newFriendshipId = await db.createFriendship([context.address, parsedRequest.user!], isActive, tx) + id = newFriendshipId + } + + await db.recordFriendshipAction( + id, + context.address, + parsedRequest.action, + parsedRequest.action === Action.REQUEST ? parsedRequest.metadata : null, + tx + ) + return id + }) + + logger.debug(`${id} friendship was upsert successfully`) + + await pubsub.publishFriendshipUpdate({ + from: context.address, + to: parsedRequest.user, + action: parsedRequest.action, + timestamp: Date.now(), + metadata: + parsedRequest.action === Action.REQUEST + ? parsedRequest.metadata + ? parsedRequest.metadata + : undefined + : undefined + }) + + return { + response: { + $case: 'accepted', + accepted: {} + } + } + } catch (error) { + logger.error(error as any) + return { + response: { + $case: 'internalServerError', + internalServerError: {} + } + } + } + } +} diff --git a/src/adapters/rpcServer.ts b/src/adapters/rpcServer.ts deleted file mode 100644 index ce56de8..0000000 --- a/src/adapters/rpcServer.ts +++ /dev/null @@ -1,323 +0,0 @@ -import { Transport, createRpcServer } from '@dcl/rpc' -import { SocialServiceDefinition } from '@dcl/protocol/out-js/decentraland/social_service_v2/social_service.gen' -import { registerService } from '@dcl/rpc/dist/codegen' -import mitt from 'mitt' -import { IBaseComponent } from '@well-known-components/interfaces' -import { - Action, - AppComponents, - Friendship, - FriendshipStatus, - RpcServerContext, - SubscriptionEventsEmitter -} from '../types' -import { - getNewFriendshipStatus, - parseEmittedUpdateToFriendshipUpdate, - parseUpsertFriendshipRequest, - validateNewFriendshipAction -} from '../logic/friendships' -import emitterToAsyncGenerator from '../utils/emitterToGenerator' -import { normalizeAddress } from '../utils/address' - -export type IRPCServerComponent = IBaseComponent & { - attachUser(user: { transport: Transport; address: string }): void -} - -const FRIENDSHIPS_COUNT_PAGE_STREAM = 20 - -const INTERNAL_SERVER_ERROR = 'SERVER ERROR' - -export default async function createRpcServerComponent( - components: Pick -): Promise { - const { logs, db, pubsub, config, server } = components - - const SHARED_CONTEXT: Pick = { - subscribers: {} - } - - const rpcServer = createRpcServer({ - logger: logs.getLogger('rpcServer') - }) - - const logger = logs.getLogger('rpcServer-handler') - - const rpcPort = (await config.getNumber('RPC_SERVER_PORT')) || 8085 - - rpcServer.setHandler(async function handler(port) { - registerService(port, SocialServiceDefinition, async () => ({ - getFriends(_request, context) { - logger.debug('getting friends for ', { address: context.address }) - let friendsGenerator: AsyncGenerator | undefined - try { - friendsGenerator = db.getFriends(context.address) - } catch (error) { - logger.error(error as any) - // throw an error bc there is no sense to create a generator to send an error - // as it's done in the previous Social Service - throw new Error(INTERNAL_SERVER_ERROR) - } - - const generator = async function* () { - let users = [] - for await (const friendship of friendsGenerator) { - const { address_requested, address_requester } = friendship - if (context.address === address_requested) { - users.push({ address: address_requester }) - } else { - users.push({ address: address_requested }) - } - - if (users.length === FRIENDSHIPS_COUNT_PAGE_STREAM) { - const response = { - users: [...users] - } - users = [] - yield response - } - } - - if (users.length) { - const response = { - users - } - yield response - } - } - - return generator() - }, - getMutualFriends(request, context) { - logger.debug(`getting mutual friends ${context.address}<>${request.user!.address}`) - let mutualFriends: AsyncGenerator<{ address: string }> | undefined - try { - mutualFriends = db.getMutualFriends(context.address, normalizeAddress(request.user!.address)) - } catch (error) { - logger.error(error as any) - // throw an error bc there is no sense to create a generator to send an error - // as it's done in the previous Social Service - throw new Error(INTERNAL_SERVER_ERROR) - } - - const generator = async function* () { - const users = [] - for await (const friendship of mutualFriends) { - const { address } = friendship - users.push({ address }) - if (users.length === FRIENDSHIPS_COUNT_PAGE_STREAM) { - const response = { - users - } - yield response - } - } - - if (users.length) { - const response = { - users - } - yield response - } - } - - return generator() - }, - async getPendingFriendshipRequests(_request, context) { - try { - const pendingRequests = await db.getReceivedFriendshipRequests(context.address) - const mappedRequests = pendingRequests.map(({ address, timestamp, metadata }) => ({ - user: { address }, - createdAt: new Date(timestamp).getTime(), - message: metadata?.message || '' - })) - - return { - response: { - $case: 'requests', - requests: { - requests: mappedRequests - } - } - } - } catch (error) { - logger.error(error as any) - return { - response: { - $case: 'internalServerError', - internalServerError: {} - } - } - } - }, - async getSentFriendshipRequests(_request, context) { - try { - const pendingRequests = await db.getSentFriendshipRequests(context.address) - const mappedRequests = pendingRequests.map(({ address, timestamp, metadata }) => ({ - user: { address }, - createdAt: new Date(timestamp).getTime(), - message: metadata?.message || '' - })) - - return { - response: { - $case: 'requests', - requests: { - requests: mappedRequests - } - } - } - } catch (error) { - logger.error(error as any) - return { - response: { - $case: 'internalServerError', - internalServerError: {} - } - } - } - }, - async upsertFriendship(request, context) { - const parsedRequest = parseUpsertFriendshipRequest(request) - if (!parsedRequest) { - logger.error('upsert friendship received unkwown message: ', request as any) - return { - response: { - $case: 'internalServerError', - internalServerError: {} - } - } - } - - logger.debug(`upsert friendship > `, parsedRequest as Record) - - try { - const friendship = await db.getFriendship([context.address, parsedRequest.user!]) - let lastAction = undefined - if (friendship) { - const lastRecordedAction = await db.getLastFriendshipAction(friendship.id) - lastAction = lastRecordedAction - } - - if ( - !validateNewFriendshipAction( - context.address, - { action: parsedRequest.action, user: parsedRequest.user }, - lastAction - ) - ) { - logger.error('invalid action for a friendship') - return { - response: { - $case: 'invalidFriendshipAction', - invalidFriendshipAction: {} - } - } - } - - const friendshipStatus = getNewFriendshipStatus(parsedRequest.action) - const isActive = friendshipStatus === FriendshipStatus.Friends - - logger.debug('friendshipstatus > ', { isActive: JSON.stringify(isActive), friendshipStatus }) - - const id = await db.executeTx(async (tx) => { - let id - if (friendship) { - await db.updateFriendshipStatus(friendship.id, isActive, tx) - id = friendship.id - } else { - const newFriendshipId = await db.createFriendship([context.address, parsedRequest.user!], isActive, tx) - id = newFriendshipId - } - - await db.recordFriendshipAction( - id, - context.address, - parsedRequest.action, - parsedRequest.action === Action.REQUEST ? parsedRequest.metadata : null, - tx - ) - return id - }) - - logger.debug(`${id} friendship was upserted successfully`) - - await pubsub.publishFriendshipUpdate({ - from: context.address, - to: parsedRequest.user, - action: parsedRequest.action, - timestamp: Date.now(), - metadata: - parsedRequest.action === Action.REQUEST - ? parsedRequest.metadata - ? parsedRequest.metadata - : undefined - : undefined - }) - - return { - response: { - $case: 'accepted', - accepted: {} - } - } - } catch (error) { - logger.error(error as any) - return { - response: { - $case: 'internalServerError', - internalServerError: {} - } - } - } - }, - subscribeToFriendshipUpdates(_request, context) { - const eventEmitter = mitt() - context.subscribers[context.address] = eventEmitter - const updatesGenerator = emitterToAsyncGenerator(eventEmitter, 'update') - - const generator = async function* () { - for await (const update of updatesGenerator) { - logger.debug('> friendship update received, sending: ', { update: update as any }) - const updateToResponse = parseEmittedUpdateToFriendshipUpdate(update) - if (updateToResponse) { - yield updateToResponse - } else { - logger.error('> unable to parse update to FriendshipUpdate > ', { update: update as any }) - } - } - } - - return generator() - } - })) - }) - - return { - async start() { - server.app.listen(rpcPort, () => { - logger.info(`[RPC] RPC Server listening on port ${rpcPort}`) - }) - - await pubsub.subscribeToFriendshipUpdates((message) => { - try { - const update = JSON.parse(message) as SubscriptionEventsEmitter['update'] - const updateEmitter = SHARED_CONTEXT.subscribers[update.to] - if (updateEmitter) { - updateEmitter.emit('update', update) - } - } catch (error) { - logger.error(error as any) - } - }) - }, - attachUser({ transport, address }) { - transport.on('close', () => { - if (SHARED_CONTEXT.subscribers[address]) { - delete SHARED_CONTEXT.subscribers[address] - } - }) - rpcServer.attachTransport(transport, { subscribers: SHARED_CONTEXT.subscribers, address }) - } - } -} diff --git a/src/adapters/ws.ts b/src/adapters/ws.ts index da31fe0..7de5a93 100644 --- a/src/adapters/ws.ts +++ b/src/adapters/ws.ts @@ -1,6 +1,7 @@ import { WebSocketServer } from 'ws' import { IWebSocketComponent } from '../types' +// TODO: UNUSED export async function createWsComponent(): Promise { let wss: WebSocketServer | undefined diff --git a/src/components.ts b/src/components.ts index c6713b9..d7d44b2 100644 --- a/src/components.ts +++ b/src/components.ts @@ -7,7 +7,7 @@ import { createPgComponent } from '@well-known-components/pg-component' import { AppComponents } from './types' import { metricDeclarations } from './metrics' import { createDBComponent } from './adapters/db' -import createRpcServerComponent from './adapters/rpcServer' +import { createRpcServerComponent } from './adapters/rpc-server' import createRedisComponent from './adapters/redis' import createPubSubComponent from './adapters/pubsub' import { createUWsComponent } from '@well-known-components/uws-http-server' diff --git a/src/types.ts b/src/types.ts index 795c491..337ca2b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -12,7 +12,7 @@ import { Emitter } from 'mitt' import { metricDeclarations } from './metrics' import { IDatabaseComponent } from './adapters/db' import { IRedisComponent } from './adapters/redis' -import { IRPCServerComponent } from './adapters/rpcServer' +import { IRPCServerComponent } from './adapters/rpc-server/rpc-server' import { IPubSubComponent } from './adapters/pubsub' import { HttpRequest, HttpResponse, IUWsComponent, WebSocket } from '@well-known-components/uws-http-server' import { IUWebSocketEventMap } from './utils/UWebSocketTransport' @@ -73,6 +73,10 @@ export type WsUserData = export type InternalWebSocket = WebSocket +export type RPCServiceContext = { + components: Pick +} + // this type simplifies the typings of http handlers export type HandlerContextWithPath< ComponentNames extends keyof AppComponents,