refactor: rpc commands

This commit is contained in:
Ben Allfree 2023-06-18 06:28:30 -07:00
parent c0b50bef94
commit f20bd11f91
5 changed files with 143 additions and 70 deletions

View File

@ -10,9 +10,9 @@ Therefore, PocketHost uses an RPC pattern instead.
## Creating a new RPC Call ## Creating a new RPC Call
1. Create a new RPC call in `./packages/common/schema/Rpc` 1. From the command line, run `npx hygen rpc new <FunctionName>`. This will generate all necessary files for both frontend and backend support of your RPC call.
2. Add frontend support in `./packages/pockethost.io/src/pocketbase/PocketbaseClient.ts` using the `mkRpc` command 2. Edit `./packages/common/src/schema/Rpc/<FunctionName>.ts` to suit the schema you want.
3. Add backend support in (for example) `./packages/daemon/src/services/InstanceService/InstanceService.ts` using `registerCommand` 3. Edit `./packages/daemon/services/RpcService/commands.ts` to respond to the RPC command
## Getting the result from an RPC call ## Getting the result from an RPC call

View File

@ -102,6 +102,7 @@ global.EventSource = require('eventsource')
pbService.shutdown() pbService.shutdown()
} }
await (await rpcService()).initRpcs()
process.on('SIGTERM', shutdown) process.on('SIGTERM', shutdown)
process.on('SIGINT', shutdown) process.on('SIGINT', shutdown)
process.on('SIGHUP', shutdown) process.on('SIGHUP', shutdown)

View File

@ -6,26 +6,16 @@ import {
PUBLIC_APP_DOMAIN, PUBLIC_APP_DOMAIN,
PUBLIC_APP_PROTOCOL, PUBLIC_APP_PROTOCOL,
} from '$constants' } from '$constants'
import { clientService, proxyService, rpcService } from '$services' import { clientService, proxyService } from '$services'
import { mkInternalUrl, now } from '$util' import { mkInternalUrl, now } from '$util'
import { import {
assertTruthy, assertTruthy,
createCleanupManager, createCleanupManager,
CreateInstancePayload,
CreateInstancePayloadSchema,
CreateInstanceResult,
createTimerManager, createTimerManager,
InstanceId, InstanceId,
InstanceStatus, InstanceStatus,
mkSingleton, mkSingleton,
RpcCommands,
safeCatch, safeCatch,
SaveSecretsPayload,
SaveSecretsPayloadSchema,
SaveSecretsResult,
SaveVersionPayload,
SaveVersionPayloadSchema,
SaveVersionResult,
SingletonBaseConfig, SingletonBaseConfig,
} from '@pockethost/common' } from '@pockethost/common'
import { forEachRight, map } from '@s-libs/micro-dash' import { forEachRight, map } from '@s-libs/micro-dash'
@ -33,7 +23,6 @@ import Bottleneck from 'bottleneck'
import { existsSync } from 'fs' import { existsSync } from 'fs'
import getPort from 'get-port' import getPort from 'get-port'
import { join } from 'path' import { join } from 'path'
import { valid, validRange } from 'semver'
import { AsyncReturnType } from 'type-fest' import { AsyncReturnType } from 'type-fest'
import { instanceLoggerService } from '../InstanceLoggerService' import { instanceLoggerService } from '../InstanceLoggerService'
import { pocketbase, PocketbaseProcess } from '../PocketBaseService' import { pocketbase, PocketbaseProcess } from '../PocketBaseService'
@ -57,59 +46,8 @@ export const instanceService = mkSingleton(
const { dbg, raw, error, warn } = _instanceLogger const { dbg, raw, error, warn } = _instanceLogger
const { client } = await clientService() const { client } = await clientService()
const { registerCommand } = await rpcService()
const pbService = await pocketbase() const pbService = await pocketbase()
registerCommand<CreateInstancePayload, CreateInstanceResult>(
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<SaveVersionPayload, SaveVersionResult>(
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<SaveSecretsPayload, SaveSecretsResult>(
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 instances: { [_: string]: InstanceApi } = {}
const instanceLimiter = new Bottleneck({ maxConcurrent: 1 }) const instanceLimiter = new Bottleneck({ maxConcurrent: 1 })

View File

@ -14,6 +14,7 @@ import Bottleneck from 'bottleneck'
import { default as knexFactory } from 'knex' import { default as knexFactory } from 'knex'
import pocketbaseEs from 'pocketbase' import pocketbaseEs from 'pocketbase'
import { AsyncReturnType, JsonObject } from 'type-fest' import { AsyncReturnType, JsonObject } from 'type-fest'
import { registerRpcCommands } from './commands'
export type RpcServiceApi = AsyncReturnType<typeof rpcService> export type RpcServiceApi = AsyncReturnType<typeof rpcService>
@ -33,7 +34,8 @@ export type RpcServiceConfig = SingletonBaseConfig & {}
export const rpcService = mkSingleton(async (config: RpcServiceConfig) => { export const rpcService = mkSingleton(async (config: RpcServiceConfig) => {
const { logger } = config const { logger } = config
const { dbg, error } = logger.create('RpcService') const rpcServiceLogger = logger.create('RpcService')
const { dbg, error } = rpcServiceLogger
const { client } = await clientService() const { client } = await clientService()
const limiter = new Bottleneck({ maxConcurrent: 1 }) 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) const unsub = await client.onNewRpc(run)
await client.resetRpcs()
const rpcs = await client.incompleteRpcs()
rpcs.forEach(run)
const shutdown = () => { const shutdown = () => {
unsub() unsub()
@ -123,6 +132,7 @@ export const rpcService = mkSingleton(async (config: RpcServiceConfig) => {
return { return {
registerCommand, registerCommand,
initRpcs,
shutdown, shutdown,
} }
}) })

View File

@ -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<CreateInstancePayload, CreateInstanceResult>(
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<SaveVersionPayload, SaveVersionResult>(
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<SaveSecretsPayload, SaveSecretsResult>(
RpcCommands.SaveSecrets,
SaveSecretsPayloadSchema,
async (job) => {
const { payload } = job
const { instanceId, secrets } = payload
await client.updateInstance(instanceId, { secrets })
return { status: 'ok' }
}
)
registerCommand<RenameInstancePayload, RenameInstanceResult>(
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<SetInstanceMaintenancePayload, SetInstanceMaintenanceResult>(
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
}