From f20bd11f91362dceef4c9bec59b8c1bf545f7efd Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 18 Jun 2023 06:28:30 -0700 Subject: [PATCH] refactor: rpc commands --- gitbook/development/rpc.md | 6 +- packages/daemon/src/server.ts | 1 + .../InstanceService/InstanceService.ts | 64 +-------- .../src/services/RpcService/RpcService.ts | 18 ++- .../src/services/RpcService/commands.ts | 124 ++++++++++++++++++ 5 files changed, 143 insertions(+), 70 deletions(-) create mode 100644 packages/daemon/src/services/RpcService/commands.ts diff --git a/gitbook/development/rpc.md b/gitbook/development/rpc.md index c403cfc7..84983290 100644 --- a/gitbook/development/rpc.md +++ b/gitbook/development/rpc.md @@ -10,9 +10,9 @@ Therefore, PocketHost uses an RPC pattern instead. ## Creating a new RPC Call -1. Create a new RPC call in `./packages/common/schema/Rpc` -2. Add frontend support in `./packages/pockethost.io/src/pocketbase/PocketbaseClient.ts` using the `mkRpc` command -3. Add backend support in (for example) `./packages/daemon/src/services/InstanceService/InstanceService.ts` using `registerCommand` +1. From the command line, run `npx hygen rpc new `. This will generate all necessary files for both frontend and backend support of your RPC call. +2. Edit `./packages/common/src/schema/Rpc/.ts` to suit the schema you want. +3. Edit `./packages/daemon/services/RpcService/commands.ts` to respond to the RPC command ## Getting the result from an RPC call diff --git a/packages/daemon/src/server.ts b/packages/daemon/src/server.ts index 82c1a83b..b01c93a3 100644 --- a/packages/daemon/src/server.ts +++ b/packages/daemon/src/server.ts @@ -102,6 +102,7 @@ global.EventSource = require('eventsource') pbService.shutdown() } + await (await rpcService()).initRpcs() process.on('SIGTERM', shutdown) process.on('SIGINT', shutdown) process.on('SIGHUP', shutdown) diff --git a/packages/daemon/src/services/InstanceService/InstanceService.ts b/packages/daemon/src/services/InstanceService/InstanceService.ts index 2923af7f..619a7990 100644 --- a/packages/daemon/src/services/InstanceService/InstanceService.ts +++ b/packages/daemon/src/services/InstanceService/InstanceService.ts @@ -6,26 +6,16 @@ import { PUBLIC_APP_DOMAIN, PUBLIC_APP_PROTOCOL, } from '$constants' -import { clientService, proxyService, rpcService } from '$services' +import { clientService, proxyService } from '$services' import { mkInternalUrl, now } from '$util' import { assertTruthy, createCleanupManager, - CreateInstancePayload, - CreateInstancePayloadSchema, - CreateInstanceResult, createTimerManager, InstanceId, InstanceStatus, mkSingleton, - RpcCommands, safeCatch, - SaveSecretsPayload, - SaveSecretsPayloadSchema, - SaveSecretsResult, - SaveVersionPayload, - SaveVersionPayloadSchema, - SaveVersionResult, SingletonBaseConfig, } from '@pockethost/common' import { forEachRight, map } from '@s-libs/micro-dash' @@ -33,7 +23,6 @@ import Bottleneck from 'bottleneck' import { existsSync } from 'fs' import getPort from 'get-port' import { join } from 'path' -import { valid, validRange } from 'semver' import { AsyncReturnType } from 'type-fest' import { instanceLoggerService } from '../InstanceLoggerService' import { pocketbase, PocketbaseProcess } from '../PocketBaseService' @@ -57,59 +46,8 @@ export const instanceService = mkSingleton( const { dbg, raw, error, warn } = _instanceLogger const { client } = await clientService() - const { registerCommand } = await rpcService() - const pbService = await pocketbase() - registerCommand( - RpcCommands.CreateInstance, - CreateInstancePayloadSchema, - async (rpc) => { - const { payload } = rpc - const { subdomain } = payload - const instance = await client.createInstance({ - subdomain, - uid: rpc.userId, - version: (await pocketbase()).getLatestVersion(), - status: InstanceStatus.Idle, - secondsThisMonth: 0, - isBackupAllowed: false, - secrets: {}, - }) - return { instance } - } - ) - - const SEMVER_RE = - /^([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+)?$/ - registerCommand( - RpcCommands.SaveVersion, - SaveVersionPayloadSchema, - async (rpc) => { - const { payload } = rpc - const { instanceId, version } = payload - if (valid(version) === null && validRange(version) === null) { - return { - status: `error`, - message: `Version must be a valid semver or semver range`, - } - } - await client.updateInstance(instanceId, { version }) - return { status: 'ok' } - } - ) - - registerCommand( - RpcCommands.SaveSecrets, - SaveSecretsPayloadSchema, - async (job) => { - const { payload } = job - const { instanceId, secrets } = payload - await client.updateInstance(instanceId, { secrets }) - return { status: 'ok' } - } - ) - const instances: { [_: string]: InstanceApi } = {} const instanceLimiter = new Bottleneck({ maxConcurrent: 1 }) diff --git a/packages/daemon/src/services/RpcService/RpcService.ts b/packages/daemon/src/services/RpcService/RpcService.ts index 9326e503..f5b553b5 100644 --- a/packages/daemon/src/services/RpcService/RpcService.ts +++ b/packages/daemon/src/services/RpcService/RpcService.ts @@ -14,6 +14,7 @@ import Bottleneck from 'bottleneck' import { default as knexFactory } from 'knex' import pocketbaseEs from 'pocketbase' import { AsyncReturnType, JsonObject } from 'type-fest' +import { registerRpcCommands } from './commands' export type RpcServiceApi = AsyncReturnType @@ -33,7 +34,8 @@ export type RpcServiceConfig = SingletonBaseConfig & {} export const rpcService = mkSingleton(async (config: RpcServiceConfig) => { const { logger } = config - const { dbg, error } = logger.create('RpcService') + const rpcServiceLogger = logger.create('RpcService') + const { dbg, error } = rpcServiceLogger const { client } = await clientService() const limiter = new Bottleneck({ maxConcurrent: 1 }) @@ -93,10 +95,17 @@ export const rpcService = mkSingleton(async (config: RpcServiceConfig) => { }) } + dbg(`Starting RPC service...`) + + const initRpcs = async () => { + dbg(`Initializing RPCs...`) + await registerRpcCommands(rpcServiceLogger) + await client.resetRpcs() + const rpcs = await client.incompleteRpcs() + rpcs.forEach(run) + } + const unsub = await client.onNewRpc(run) - await client.resetRpcs() - const rpcs = await client.incompleteRpcs() - rpcs.forEach(run) const shutdown = () => { unsub() @@ -123,6 +132,7 @@ export const rpcService = mkSingleton(async (config: RpcServiceConfig) => { return { registerCommand, + initRpcs, shutdown, } }) diff --git a/packages/daemon/src/services/RpcService/commands.ts b/packages/daemon/src/services/RpcService/commands.ts new file mode 100644 index 00000000..3c9c4f84 --- /dev/null +++ b/packages/daemon/src/services/RpcService/commands.ts @@ -0,0 +1,124 @@ +import { + CreateInstancePayload, + CreateInstancePayloadSchema, + CreateInstanceResult, + InstanceStatus, + Logger, + RenameInstancePayloadSchema, + RpcCommands, + SaveSecretsPayload, + SaveSecretsPayloadSchema, + SaveSecretsResult, + SaveVersionPayload, + SaveVersionPayloadSchema, + // gen:import + SaveVersionResult, + SetInstanceMaintenancePayloadSchema, + type RenameInstancePayload, + type RenameInstanceResult, + type SetInstanceMaintenancePayload, + type SetInstanceMaintenanceResult, +} from '@pockethost/common' +import { valid, validRange } from 'semver' +import { clientService } from '../clientService/clientService' +import { instanceService } from '../InstanceService/InstanceService' +import { pocketbase } from '../PocketBaseService' +import { rpcService } from './RpcService' + +export const registerRpcCommands = async (logger: Logger) => { + const { client } = await clientService() + const _rpcCommandLogger = logger.create(`RpcCommands`) + const { dbg, warn } = _rpcCommandLogger + + const { registerCommand } = await rpcService() + + registerCommand( + RpcCommands.CreateInstance, + CreateInstancePayloadSchema, + async (rpc) => { + const { payload } = rpc + const { subdomain } = payload + const instance = await client.createInstance({ + subdomain, + uid: rpc.userId, + version: (await pocketbase()).getLatestVersion(), + status: InstanceStatus.Idle, + secondsThisMonth: 0, + secrets: {}, + maintenance: false, + }) + return { instance } + } + ) + + registerCommand( + RpcCommands.SaveVersion, + SaveVersionPayloadSchema, + async (rpc) => { + const { payload } = rpc + const { instanceId, version } = payload + if (valid(version) === null && validRange(version) === null) { + return { + status: `error`, + message: `Version must be a valid semver or semver range`, + } + } + await client.updateInstance(instanceId, { version }) + return { status: 'ok' } + } + ) + + registerCommand( + RpcCommands.SaveSecrets, + SaveSecretsPayloadSchema, + async (job) => { + const { payload } = job + const { instanceId, secrets } = payload + await client.updateInstance(instanceId, { secrets }) + return { status: 'ok' } + } + ) + + registerCommand( + RpcCommands.RenameInstance, + RenameInstancePayloadSchema, + async (job) => { + const { payload } = job + const { instanceId, subdomain } = payload + try { + await client.updateInstance(instanceId, { subdomain }) + return { status: 'ok' } + } catch (e) { + return { status: 'error', message: `${e}` } + } + } + ) + + registerCommand( + RpcCommands.SetInstanceMaintenance, + SetInstanceMaintenancePayloadSchema, + async (job) => { + const { payload } = job + const { instanceId, maintenance } = payload + try { + dbg(`Updating to maintenance mode ${instanceId}`) + await client.updateInstance(instanceId, { maintenance }) + if (maintenance) { + try { + dbg(`Shutting down instance ${instanceId}`) + const is = await instanceService() + const api = is.getInstanceApiIfExistsById(instanceId) + await api?.shutdown() + } catch (e) { + warn(e) + } + } + return { status: 'ok' } + } catch (e) { + return { status: 'error', message: `${e}` } + } + } + ) + + // gen:command +}