Skip to content

Commit 5c6d612

Browse files
committed
feat: implement rpc architecture
1 parent b0b7aad commit 5c6d612

7 files changed

Lines changed: 670 additions & 7 deletions

File tree

packages/core/src/bridge/handlers/command-handlers.ts

Lines changed: 147 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { NodeHeartbeatPayload, NodeRegisterPayload } from '@frp-bridge/type
22
import type { NodeManager } from '../../node'
33
import type { FrpProcessManager } from '../../process'
44
import type { RpcServer } from '../../rpc'
5+
import type { NodeDeletePayload, TunnelAddPayload, TunnelDeletePayload } from '../../rpc/message-types'
56
import type { CommandHandler, CommandHandlerContext, CommandResult, RuntimeEvent } from '../../runtime'
67
import type { ConfigApplyPayload, ConfigApplyRawPayload, ProxyAddPayload, ProxyRemovePayload, ProxyUpdatePayload } from '../types'
78
import type { Validator } from './decorators'
@@ -470,8 +471,153 @@ export function createCommandHandlers(deps: CommandDependencies): Record<string,
470471
'node.register': createNodeRegisterCommand(deps) as CommandHandler,
471472
'node.heartbeat': createNodeHeartbeatCommand(deps) as CommandHandler,
472473
'node.unregister': createNodeUnregisterCommand(deps) as CommandHandler,
474+
'node.delete': createNodeDeleteCommand(deps) as CommandHandler,
473475
'proxy.add': createProxyAddCommand(deps) as CommandHandler,
474476
'proxy.update': createProxyUpdateCommand(deps) as CommandHandler,
475-
'proxy.remove': createProxyRemoveCommand(deps) as CommandHandler
477+
'proxy.remove': createProxyRemoveCommand(deps) as CommandHandler,
478+
// Tunnel commands (aliases for proxy commands, matching document spec)
479+
'tunnel.add': createTunnelAddCommand(deps) as CommandHandler,
480+
'tunnel.delete': createTunnelDeleteCommand(deps) as CommandHandler
476481
}
477482
}
483+
484+
// ============================================================================
485+
// Tunnel Commands (matching document spec)
486+
// ============================================================================
487+
488+
/**
489+
* Validate tunnel add payload
490+
*/
491+
const validateTunnelAdd: Validator<TunnelAddPayload> = (payload) => {
492+
if (!payload) {
493+
return { valid: false, error: 'tunnel.add requires payload' }
494+
}
495+
if (!payload.name || typeof payload.name !== 'string') {
496+
return { valid: false, error: 'tunnel.add requires payload.name' }
497+
}
498+
if (!payload.type || typeof payload.type !== 'string') {
499+
return { valid: false, error: 'tunnel.add requires payload.type' }
500+
}
501+
if (typeof payload.localPort !== 'number') {
502+
return { valid: false, error: 'tunnel.add requires payload.localPort to be a number' }
503+
}
504+
return { valid: true }
505+
}
506+
507+
/**
508+
* Validate tunnel delete payload
509+
*/
510+
const validateTunnelDelete: Validator<TunnelDeletePayload> = (payload) => {
511+
if (!payload?.name) {
512+
return { valid: false, error: 'tunnel.delete requires payload.name' }
513+
}
514+
return { valid: true }
515+
}
516+
517+
/**
518+
* Validate node delete payload
519+
*/
520+
const validateNodeDelete: Validator<NodeDeletePayload> = (payload) => {
521+
if (!payload?.name) {
522+
return { valid: false, error: 'node.delete requires payload.name' }
523+
}
524+
return { valid: true }
525+
}
526+
527+
/**
528+
* Local tunnel add handler (client mode)
529+
* Compatible with document spec: { name, type, localPort, remotePort?, ... }
530+
*/
531+
async function tunnelAddLocal(payload: TunnelAddPayload, deps: CommandDependencies): Promise<CommandResult> {
532+
// Convert TunnelAddPayload to ProxyConfig format
533+
const proxyConfig = {
534+
name: payload.name,
535+
type: payload.type,
536+
localIP: '127.0.0.1',
537+
localPort: payload.localPort,
538+
...(payload.remotePort && { remotePort: payload.remotePort }),
539+
...(payload.customDomains && { customDomains: payload.customDomains }),
540+
...(payload.subdomain && { subdomain: payload.subdomain })
541+
}
542+
543+
deps.process.addTunnel(proxyConfig)
544+
return {
545+
status: 'success',
546+
result: { ...proxyConfig, success: true }
547+
}
548+
}
549+
550+
/**
551+
* Create tunnel add command handler (matching document spec)
552+
* This is an alias for proxy.add with different payload format
553+
*/
554+
export function createTunnelAddCommand(deps: CommandDependencies): CommandHandler<TunnelAddPayload> {
555+
return compose<TunnelAddPayload>(
556+
withErrorHandling,
557+
withValidation(validateTunnelAdd),
558+
withPortConflictCheck
559+
)(
560+
withModeRouting(
561+
tunnelAddLocal,
562+
'tunnel.add',
563+
payload => ({ proxy: payload as any })
564+
)(async () => ({ status: 'success' }), deps),
565+
deps
566+
)
567+
}
568+
569+
/**
570+
* Local tunnel delete handler (client mode)
571+
*/
572+
async function tunnelDeleteLocal(payload: TunnelDeletePayload, deps: CommandDependencies): Promise<CommandResult> {
573+
deps.process.removeTunnel(payload.name)
574+
return {
575+
status: 'success',
576+
result: { name: payload.name, success: true }
577+
}
578+
}
579+
580+
/**
581+
* Create tunnel delete command handler (matching document spec)
582+
* This is an alias for proxy.remove
583+
*/
584+
export function createTunnelDeleteCommand(deps: CommandDependencies): CommandHandler<TunnelDeletePayload> {
585+
return compose<TunnelDeletePayload>(
586+
withErrorHandling,
587+
withValidation(validateTunnelDelete)
588+
)(
589+
withModeRouting(
590+
tunnelDeleteLocal,
591+
'tunnel.delete',
592+
payload => ({ name: payload.name })
593+
)(async () => ({ status: 'success' }), deps),
594+
deps
595+
)
596+
}
597+
598+
/**
599+
* Node delete handler (server mode only)
600+
* Removes a node from the node manager
601+
*/
602+
function nodeDeleteCore(deps: CommandDependencies): CommandHandler<NodeDeletePayload> {
603+
return async (command) => {
604+
const nodeName = command.payload!.name
605+
deps.nodeManager!.unregisterNode(nodeName)
606+
return {
607+
status: 'success',
608+
result: { deletedNode: nodeName, success: true }
609+
}
610+
}
611+
}
612+
613+
/**
614+
* Create node delete command handler (matching document spec)
615+
*/
616+
export function createNodeDeleteCommand(deps: CommandDependencies): CommandHandler<NodeDeletePayload> {
617+
return compose<NodeDeletePayload>(
618+
withErrorHandling,
619+
withNodeManager,
620+
withServerModeOnly,
621+
withValidation(validateNodeDelete)
622+
)(nodeDeleteCore(deps), deps)
623+
}

packages/core/src/rpc/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ export * from './message-types'
22
export * from './middleware'
33
export * from './reconnect-strategy'
44
export { RpcClient, type RpcClientOptions } from './rpc-client'
5-
export { RpcServer, type RpcServerOptions } from './rpc-server'
5+
export { type RpcCommandStatus, RpcServer, type RpcServerOptions } from './rpc-server'

packages/core/src/rpc/message-types.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,87 @@ export interface PongMessage {
6969
*/
7070
export type RpcMessage = RegisterMessage | RpcRequest | RpcResponse | PingMessage | PongMessage
7171

72+
// ============================================================================
73+
// Event-based Message Types (compatible with architecture document)
74+
// ============================================================================
75+
76+
/**
77+
* Event-based RPC message type (matching document spec)
78+
*/
79+
export interface EventRpcMessage {
80+
type: 'command' | 'event'
81+
action: string
82+
payload: unknown
83+
id?: string
84+
targetNodeId?: string
85+
}
86+
87+
/**
88+
* Command message (frps -> frpc)
89+
*/
90+
export interface CommandRpcMessage extends EventRpcMessage {
91+
type: 'command'
92+
id: string
93+
}
94+
95+
/**
96+
* Event message (frpc -> frps)
97+
*/
98+
export interface EventRpcMessageEvent extends EventRpcMessage {
99+
type: 'event'
100+
id?: string
101+
}
102+
103+
/**
104+
* Tunnel add payload
105+
*/
106+
export interface TunnelAddPayload {
107+
name: string
108+
type: 'tcp' | 'http' | 'https' | 'stcp' | 'sudp' | 'xtcp'
109+
localPort: number
110+
remotePort?: number
111+
customDomains?: string[]
112+
subdomain?: string
113+
[key: string]: unknown
114+
}
115+
116+
/**
117+
* Tunnel delete payload
118+
*/
119+
export interface TunnelDeletePayload {
120+
name: string
121+
}
122+
123+
/**
124+
* Tunnel response payload
125+
*/
126+
export interface TunnelResponsePayload {
127+
success: boolean
128+
error?: string
129+
tunnel?: TunnelAddPayload
130+
}
131+
132+
/**
133+
* Node delete payload
134+
*/
135+
export interface NodeDeletePayload {
136+
name: string
137+
}
138+
139+
/**
140+
* Node response payload
141+
*/
142+
export interface NodeResponsePayload {
143+
success: boolean
144+
error?: string
145+
deletedNode?: string
146+
}
147+
148+
/**
149+
* All message types including event-based
150+
*/
151+
export type AllRpcMessage = RpcMessage | EventRpcMessage
152+
72153
/**
73154
* 类型守卫:检查是否为注册消息
74155
*/
@@ -109,3 +190,64 @@ export function isPongMessage(msg: unknown): msg is PongMessage {
109190
return typeof msg === 'object' && msg !== null
110191
&& (msg as any).type === RpcMessageType.PONG
111192
}
193+
194+
// ============================================================================
195+
// Event-based Message Type Guards
196+
// ============================================================================
197+
198+
/**
199+
* 类型守卫:检查是否为 Event-based RPC 消息
200+
*/
201+
export function isEventRpcMessage(msg: unknown): msg is EventRpcMessage {
202+
return typeof msg === 'object' && msg !== null
203+
&& ((msg as any).type === 'command' || (msg as any).type === 'event')
204+
&& typeof (msg as any).action === 'string'
205+
&& 'payload' in (msg as any)
206+
}
207+
208+
/**
209+
* 类型守卫:检查是否为 Command 消息
210+
*/
211+
export function isCommandMessage(msg: unknown): msg is CommandRpcMessage {
212+
return isEventRpcMessage(msg)
213+
&& (msg as any).type === 'command'
214+
&& typeof (msg as any).id === 'string'
215+
}
216+
217+
/**
218+
* 类型守卫:检查是否为 Event 消息
219+
*/
220+
export function isEventMessage(msg: unknown): msg is EventRpcMessageEvent {
221+
return isEventRpcMessage(msg)
222+
&& (msg as any).type === 'event'
223+
}
224+
225+
/**
226+
* 类型守卫:检查是否为 TunnelAddPayload
227+
*/
228+
export function isTunnelAddPayload(data: unknown): data is TunnelAddPayload {
229+
if (typeof data !== 'object' || data === null)
230+
return false
231+
232+
const payload = data as TunnelAddPayload
233+
return typeof payload.name === 'string'
234+
&& typeof payload.type === 'string'
235+
&& typeof payload.localPort === 'number'
236+
&& (payload.remotePort === undefined || typeof payload.remotePort === 'number')
237+
}
238+
239+
/**
240+
* 类型守卫:检查是否为 TunnelDeletePayload
241+
*/
242+
export function isTunnelDeletePayload(data: unknown): data is TunnelDeletePayload {
243+
return typeof data === 'object' && data !== null
244+
&& typeof (data as any).name === 'string'
245+
}
246+
247+
/**
248+
* 类型守卫:检查是否为 NodeDeletePayload
249+
*/
250+
export function isNodeDeletePayload(data: unknown): data is NodeDeletePayload {
251+
return typeof data === 'object' && data !== null
252+
&& typeof (data as any).name === 'string'
253+
}

0 commit comments

Comments
 (0)