@anycon/protocol 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +176 -0
- package/README.md +146 -0
- package/README.zh-CN.md +118 -0
- package/dist/constants.d.ts +151 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +159 -0
- package/dist/constants.js.map +1 -0
- package/dist/crypto.d.ts +202 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +218 -0
- package/dist/crypto.js.map +1 -0
- package/dist/envelope.d.ts +105 -0
- package/dist/envelope.d.ts.map +1 -0
- package/dist/envelope.js +390 -0
- package/dist/envelope.js.map +1 -0
- package/dist/errors.d.ts +5 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +5 -0
- package/dist/errors.js.map +1 -0
- package/dist/generated.d.ts +2007 -0
- package/dist/generated.d.ts.map +1 -0
- package/dist/generated.js +4 -0
- package/dist/generated.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/method-types.d.ts +293 -0
- package/dist/method-types.d.ts.map +1 -0
- package/dist/method-types.js +2 -0
- package/dist/method-types.js.map +1 -0
- package/dist/rpc.d.ts +70 -0
- package/dist/rpc.d.ts.map +1 -0
- package/dist/rpc.js +310 -0
- package/dist/rpc.js.map +1 -0
- package/package.json +62 -0
- package/src/constants.ts +173 -0
- package/src/crypto.ts +427 -0
- package/src/envelope.ts +469 -0
- package/src/errors.ts +4 -0
- package/src/generated.ts +2094 -0
- package/src/index.ts +18 -0
- package/src/method-types.ts +183 -0
- package/src/rpc.ts +377 -0
- package/wasm/THIRD-PARTY-LICENSES.txt +1957 -0
- package/wasm/anycon_crypto.d.ts +248 -0
- package/wasm/anycon_crypto.js +1250 -0
- package/wasm/anycon_crypto_bg.wasm +0 -0
- package/wasm/anycon_crypto_bg.wasm.d.ts +48 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@anycon/protocol`:anycon 协议的 TypeScript 侧。
|
|
3
|
+
*
|
|
4
|
+
* - 生成的类型(`generated.ts`,来自 `crates/protocol` 的 JSON Schema)
|
|
5
|
+
* - 外层信封 / 控制消息的 msgpack 编解码(`envelope.ts`)
|
|
6
|
+
* - 内层 JSON-RPC 编解码与 `RpcPeer`(`rpc.ts`)
|
|
7
|
+
* - Noise wasm 封装(`crypto.ts`)
|
|
8
|
+
* - 常量(`constants.ts`)
|
|
9
|
+
*/
|
|
10
|
+
export * from './constants.js'
|
|
11
|
+
export * from './crypto.js'
|
|
12
|
+
export * from './envelope.js'
|
|
13
|
+
export * from './errors.js'
|
|
14
|
+
export * from './method-types.js'
|
|
15
|
+
export * from './rpc.js'
|
|
16
|
+
|
|
17
|
+
// 生成的类型。`Control`、`RpcMessage` 等已经由上面的模块导出,这里只补充其余的。
|
|
18
|
+
export type * from './generated.js'
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 方法名 → params / result 的类型表,供 `RpcPeer.request` 之上的类型化封装用。
|
|
3
|
+
*
|
|
4
|
+
* 由 `just gen-ts` 从 `schema/methods.json`(`crates/protocol/src/methods.rs` 的 `REGISTRY`)生成,
|
|
5
|
+
* 不要手改;`tests/method-types.test.ts` 会断言它与 schema 同步。类型名来自 `generated.ts`。
|
|
6
|
+
* 没有参数 / 没有结果的方法记作 `Empty`(线上是 `{}`)。
|
|
7
|
+
*/
|
|
8
|
+
import type {
|
|
9
|
+
AgentDiagnoseParams,
|
|
10
|
+
AgentDiagnoseResult,
|
|
11
|
+
AgentListParams,
|
|
12
|
+
AgentListResult,
|
|
13
|
+
AttachmentRef,
|
|
14
|
+
BlobGetParams,
|
|
15
|
+
BlobGetResult,
|
|
16
|
+
BlobIdParams,
|
|
17
|
+
BlobPutBeginParams,
|
|
18
|
+
BlobPutBeginResult,
|
|
19
|
+
BlobPutChunkParams,
|
|
20
|
+
Empty,
|
|
21
|
+
EnrollInfo,
|
|
22
|
+
FsListParams,
|
|
23
|
+
FsListResult,
|
|
24
|
+
FsReadParams,
|
|
25
|
+
FsReadResult,
|
|
26
|
+
JobCancelParams,
|
|
27
|
+
JobDone,
|
|
28
|
+
JobProgress,
|
|
29
|
+
JobStarted,
|
|
30
|
+
NodeAuditParams,
|
|
31
|
+
NodeAuditResult,
|
|
32
|
+
NodeDevicesResult,
|
|
33
|
+
NodeDoctorResult,
|
|
34
|
+
NodeEnrollApproveParams,
|
|
35
|
+
NodeEnrollApproveResult,
|
|
36
|
+
NodeEnrollCodeResult,
|
|
37
|
+
NodeEnrollIdParams,
|
|
38
|
+
NodeEnrollListResult,
|
|
39
|
+
NodeEnrollResolved,
|
|
40
|
+
NodeEnrollStatusResult,
|
|
41
|
+
NodeInfo,
|
|
42
|
+
NodePairBeginParams,
|
|
43
|
+
NodePairBeginResult,
|
|
44
|
+
NodeRevokeDeviceParams,
|
|
45
|
+
NodeSecurityEvent,
|
|
46
|
+
NodeSetAgentDefaultsParams,
|
|
47
|
+
NodeSetEnrollEnabledParams,
|
|
48
|
+
NodeSetEnrollPasswordParams,
|
|
49
|
+
NodeStats,
|
|
50
|
+
PairAck,
|
|
51
|
+
PermissionListParams,
|
|
52
|
+
PermissionListResult,
|
|
53
|
+
PermissionRequest,
|
|
54
|
+
PermissionResolved,
|
|
55
|
+
PermissionRespondParams,
|
|
56
|
+
Project,
|
|
57
|
+
RenamedProject,
|
|
58
|
+
SessionArchiveParams,
|
|
59
|
+
SessionChanged,
|
|
60
|
+
SessionCreateParams,
|
|
61
|
+
SessionDeleteParams,
|
|
62
|
+
SessionDiffParams,
|
|
63
|
+
SessionDiffResult,
|
|
64
|
+
SessionEvent,
|
|
65
|
+
SessionGitStatus,
|
|
66
|
+
SessionGitStatusParams,
|
|
67
|
+
SessionHistoryParams,
|
|
68
|
+
SessionHistoryResult,
|
|
69
|
+
SessionIdParams,
|
|
70
|
+
SessionListParams,
|
|
71
|
+
SessionListResult,
|
|
72
|
+
SessionPromoteParams,
|
|
73
|
+
SessionRenameParams,
|
|
74
|
+
SessionSearchParams,
|
|
75
|
+
SessionSearchResult,
|
|
76
|
+
SessionSendParams,
|
|
77
|
+
SessionSendResult,
|
|
78
|
+
SessionSetSettingsParams,
|
|
79
|
+
SessionSetSettingsResult,
|
|
80
|
+
SessionSummary,
|
|
81
|
+
SessionSyncBaseParams,
|
|
82
|
+
SessionSyncBaseResult,
|
|
83
|
+
TrashEntry,
|
|
84
|
+
WorkspaceBranchesParams,
|
|
85
|
+
WorkspaceBranchesResult,
|
|
86
|
+
WorkspaceBrowseParams,
|
|
87
|
+
WorkspaceBrowseResult,
|
|
88
|
+
WorkspaceChanged,
|
|
89
|
+
WorkspaceCloneParams,
|
|
90
|
+
WorkspaceCreateParams,
|
|
91
|
+
WorkspaceLinkParams,
|
|
92
|
+
WorkspaceListResult,
|
|
93
|
+
WorkspaceNameParams,
|
|
94
|
+
WorkspaceRemoveWorktreeParams,
|
|
95
|
+
WorkspaceRenameParams,
|
|
96
|
+
WorkspaceTrashIdParams,
|
|
97
|
+
WorkspaceTrashListResult,
|
|
98
|
+
WorkspaceWorktreesParams,
|
|
99
|
+
WorkspaceWorktreesResult,
|
|
100
|
+
} from './generated.js'
|
|
101
|
+
|
|
102
|
+
/** C→D 请求方法。 */
|
|
103
|
+
export interface RequestMethods {
|
|
104
|
+
'node.info': { params: Empty; result: NodeInfo }
|
|
105
|
+
'node.devices': { params: Empty; result: NodeDevicesResult }
|
|
106
|
+
'node.revoke_device': { params: NodeRevokeDeviceParams; result: Empty }
|
|
107
|
+
'node.pair_begin': { params: NodePairBeginParams; result: NodePairBeginResult }
|
|
108
|
+
'node.stats': { params: Empty; result: NodeStats }
|
|
109
|
+
'node.doctor': { params: Empty; result: NodeDoctorResult }
|
|
110
|
+
'node.audit': { params: NodeAuditParams; result: NodeAuditResult }
|
|
111
|
+
'node.set_agent_defaults': { params: NodeSetAgentDefaultsParams; result: Empty }
|
|
112
|
+
'node.enroll_list': { params: Empty; result: NodeEnrollListResult }
|
|
113
|
+
'node.enroll_approve': { params: NodeEnrollApproveParams; result: NodeEnrollApproveResult }
|
|
114
|
+
'node.enroll_deny': { params: NodeEnrollIdParams; result: Empty }
|
|
115
|
+
'node.enroll_code': { params: Empty; result: NodeEnrollCodeResult }
|
|
116
|
+
'node.enroll_status': { params: Empty; result: NodeEnrollStatusResult }
|
|
117
|
+
'node.set_enroll_password': { params: NodeSetEnrollPasswordParams; result: Empty }
|
|
118
|
+
'node.set_enroll_enabled': { params: NodeSetEnrollEnabledParams; result: Empty }
|
|
119
|
+
'agent.list': { params: AgentListParams; result: AgentListResult }
|
|
120
|
+
'agent.diagnose': { params: AgentDiagnoseParams; result: AgentDiagnoseResult }
|
|
121
|
+
'workspace.list': { params: Empty; result: WorkspaceListResult }
|
|
122
|
+
'workspace.create': { params: WorkspaceCreateParams; result: Project }
|
|
123
|
+
'workspace.clone': { params: WorkspaceCloneParams; result: JobStarted }
|
|
124
|
+
'workspace.rename': { params: WorkspaceRenameParams; result: RenamedProject }
|
|
125
|
+
'workspace.trash': { params: WorkspaceNameParams; result: TrashEntry }
|
|
126
|
+
'workspace.trash_list': { params: Empty; result: WorkspaceTrashListResult }
|
|
127
|
+
'workspace.restore': { params: WorkspaceTrashIdParams; result: Project }
|
|
128
|
+
'workspace.purge': { params: WorkspaceTrashIdParams; result: Empty }
|
|
129
|
+
'workspace.link': { params: WorkspaceLinkParams; result: Project }
|
|
130
|
+
'workspace.unlink': { params: WorkspaceNameParams; result: Empty }
|
|
131
|
+
'workspace.browse': { params: WorkspaceBrowseParams; result: WorkspaceBrowseResult }
|
|
132
|
+
'workspace.branches': { params: WorkspaceBranchesParams; result: WorkspaceBranchesResult }
|
|
133
|
+
'workspace.worktrees': { params: WorkspaceWorktreesParams; result: WorkspaceWorktreesResult }
|
|
134
|
+
'workspace.remove_worktree': { params: WorkspaceRemoveWorktreeParams; result: Empty }
|
|
135
|
+
'job.cancel': { params: JobCancelParams; result: Empty }
|
|
136
|
+
'fs.list': { params: FsListParams; result: FsListResult }
|
|
137
|
+
'fs.read': { params: FsReadParams; result: FsReadResult }
|
|
138
|
+
'session.list': { params: SessionListParams; result: SessionListResult }
|
|
139
|
+
'session.get': { params: SessionIdParams; result: SessionSummary }
|
|
140
|
+
'session.create': { params: SessionCreateParams; result: SessionSummary }
|
|
141
|
+
'session.send': { params: SessionSendParams; result: SessionSendResult }
|
|
142
|
+
'session.cancel': { params: SessionIdParams; result: Empty }
|
|
143
|
+
'session.set_settings': { params: SessionSetSettingsParams; result: SessionSetSettingsResult }
|
|
144
|
+
'session.watch': { params: SessionIdParams; result: Empty }
|
|
145
|
+
'session.unwatch': { params: SessionIdParams; result: Empty }
|
|
146
|
+
'session.history': { params: SessionHistoryParams; result: SessionHistoryResult }
|
|
147
|
+
'session.rename': { params: SessionRenameParams; result: Empty }
|
|
148
|
+
'session.archive': { params: SessionArchiveParams; result: Empty }
|
|
149
|
+
'session.unarchive': { params: SessionIdParams; result: Empty }
|
|
150
|
+
'session.delete': { params: SessionDeleteParams; result: Empty }
|
|
151
|
+
'session.restart': { params: SessionIdParams; result: Empty }
|
|
152
|
+
'session.search': { params: SessionSearchParams; result: SessionSearchResult }
|
|
153
|
+
'session.promote': { params: SessionPromoteParams; result: Empty }
|
|
154
|
+
'session.git_status': { params: SessionGitStatusParams; result: SessionGitStatus }
|
|
155
|
+
'session.diff': { params: SessionDiffParams; result: SessionDiffResult }
|
|
156
|
+
'session.sync_base': { params: SessionSyncBaseParams; result: SessionSyncBaseResult }
|
|
157
|
+
'blob.put_begin': { params: BlobPutBeginParams; result: BlobPutBeginResult }
|
|
158
|
+
'blob.put_chunk': { params: BlobPutChunkParams; result: Empty }
|
|
159
|
+
'blob.put_end': { params: BlobIdParams; result: AttachmentRef }
|
|
160
|
+
'blob.get': { params: BlobGetParams; result: BlobGetResult }
|
|
161
|
+
'blob.discard': { params: BlobIdParams; result: Empty }
|
|
162
|
+
'permission.respond': { params: PermissionRespondParams; result: Empty }
|
|
163
|
+
'permission.list': { params: PermissionListParams; result: PermissionListResult }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** D→C 通知方法(没有响应)。 */
|
|
167
|
+
export interface NotificationMethods {
|
|
168
|
+
'pair.ack': { params: PairAck }
|
|
169
|
+
'node.enroll_requested': { params: EnrollInfo }
|
|
170
|
+
'node.enroll_resolved': { params: NodeEnrollResolved }
|
|
171
|
+
'node.security_event': { params: NodeSecurityEvent }
|
|
172
|
+
'agent.changed': { params: AgentListResult }
|
|
173
|
+
'workspace.changed': { params: WorkspaceChanged }
|
|
174
|
+
'job.progress': { params: JobProgress }
|
|
175
|
+
'job.done': { params: JobDone }
|
|
176
|
+
'session.event': { params: SessionEvent }
|
|
177
|
+
'session.changed': { params: SessionChanged }
|
|
178
|
+
'permission.request': { params: PermissionRequest }
|
|
179
|
+
'permission.resolved': { params: PermissionResolved }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export type RequestMethodName = keyof RequestMethods
|
|
183
|
+
export type NotificationMethodName = keyof NotificationMethods
|
package/src/rpc.ts
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 内层 JSON-RPC 2.0:类型、编解码、类型守卫,以及与传输无关的 `RpcPeer`。
|
|
3
|
+
*
|
|
4
|
+
* 一个 `data` 信封 = 一条 JSON-RPC 消息(请求 / 响应 / 通知之一),不合并。
|
|
5
|
+
* 错误以 `error.data.code`(稳定 snake_case)为准,`AnyconRpcError.code` 直接给出它。
|
|
6
|
+
*/
|
|
7
|
+
import { ERROR_CODE_NUMBERS } from './constants.js'
|
|
8
|
+
import { ProtocolError } from './errors.js'
|
|
9
|
+
import type {
|
|
10
|
+
ErrorCode,
|
|
11
|
+
RpcError,
|
|
12
|
+
RpcErrorData,
|
|
13
|
+
RpcId,
|
|
14
|
+
RpcMessage,
|
|
15
|
+
RpcNotification,
|
|
16
|
+
RpcRequest,
|
|
17
|
+
RpcResponse,
|
|
18
|
+
} from './generated.js'
|
|
19
|
+
|
|
20
|
+
export type {
|
|
21
|
+
ErrorCode,
|
|
22
|
+
RpcError,
|
|
23
|
+
RpcErrorData,
|
|
24
|
+
RpcId,
|
|
25
|
+
RpcMessage,
|
|
26
|
+
RpcNotification,
|
|
27
|
+
RpcRequest,
|
|
28
|
+
RpcResponse,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const JSONRPC = '2.0'
|
|
32
|
+
|
|
33
|
+
// ---------- 守卫 ----------
|
|
34
|
+
|
|
35
|
+
/** 有 `method` 且有 `id`。 */
|
|
36
|
+
export function isRequest(m: RpcMessage): m is RpcRequest {
|
|
37
|
+
return 'method' in m && 'id' in m && m.id !== undefined && m.id !== null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 有 `method` 无 `id`。 */
|
|
41
|
+
export function isNotification(m: RpcMessage): m is RpcNotification {
|
|
42
|
+
return 'method' in m && !('id' in m && m.id !== undefined && m.id !== null)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 没有 `method`。 */
|
|
46
|
+
export function isResponse(m: RpcMessage): m is RpcResponse {
|
|
47
|
+
return !('method' in m)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ---------- 构造(键序与 Rust 结构体声明一致) ----------
|
|
51
|
+
|
|
52
|
+
export function makeRequest(id: RpcId, method: string, params: unknown = {}): RpcRequest {
|
|
53
|
+
return { jsonrpc: JSONRPC, id, method, params: params as RpcRequest['params'] }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function makeNotification(method: string, params: unknown = {}): RpcNotification {
|
|
57
|
+
return { jsonrpc: JSONRPC, method, params: params as RpcNotification['params'] }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function makeResult(id: RpcId, result: unknown): RpcResponse {
|
|
61
|
+
return { jsonrpc: JSONRPC, id, result }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function makeError(id: RpcId, error: RpcError): RpcResponse {
|
|
65
|
+
return { jsonrpc: JSONRPC, id, error }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---------- 编解码 ----------
|
|
69
|
+
|
|
70
|
+
const textEncoder = new TextEncoder()
|
|
71
|
+
const textDecoder = new TextDecoder('utf-8', { fatal: true })
|
|
72
|
+
|
|
73
|
+
const isSet = <T>(v: T | null | undefined): v is T => v !== null && v !== undefined
|
|
74
|
+
|
|
75
|
+
/** `RpcError` 的字段顺序 code, message, data;`data` 里的可选字段为 null / undefined 时省略。 */
|
|
76
|
+
function canonicalError(err: RpcError): RpcError {
|
|
77
|
+
const out: RpcError = { code: err.code, message: err.message }
|
|
78
|
+
const data = err.data
|
|
79
|
+
if (data !== undefined && data !== null) {
|
|
80
|
+
const d: RpcErrorData = { code: data.code }
|
|
81
|
+
if (isSet(data.field)) d.field = data.field
|
|
82
|
+
if (isSet(data.reason)) d.reason = data.reason
|
|
83
|
+
if (isSet(data.retry_after_ms)) d.retry_after_ms = data.retry_after_ms
|
|
84
|
+
out.data = d
|
|
85
|
+
}
|
|
86
|
+
return out
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 按 Rust 侧的字段顺序重排,保证和 `serde_json::to_vec` 字节一致。 */
|
|
90
|
+
function canonicalRpc(msg: RpcMessage): RpcMessage {
|
|
91
|
+
if (isRequest(msg)) return makeRequest(msg.id, msg.method, msg.params ?? {})
|
|
92
|
+
if (isNotification(msg)) return makeNotification(msg.method, msg.params ?? {})
|
|
93
|
+
const out: RpcResponse = { jsonrpc: JSONRPC, id: msg.id }
|
|
94
|
+
if (msg.result !== undefined) out.result = msg.result
|
|
95
|
+
if (msg.error !== undefined && msg.error !== null) out.error = canonicalError(msg.error)
|
|
96
|
+
return out
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function encodeRpc(msg: RpcMessage): Uint8Array {
|
|
100
|
+
return textEncoder.encode(JSON.stringify(canonicalRpc(msg)))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function decodeRpc(bytes: Uint8Array): RpcMessage {
|
|
104
|
+
let value: unknown
|
|
105
|
+
try {
|
|
106
|
+
value = JSON.parse(textDecoder.decode(bytes))
|
|
107
|
+
} catch (cause) {
|
|
108
|
+
throw new ProtocolError('rpc message is not valid JSON', { cause })
|
|
109
|
+
}
|
|
110
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
111
|
+
throw new ProtocolError('rpc message must be a JSON object')
|
|
112
|
+
}
|
|
113
|
+
const m = value as Record<string, unknown>
|
|
114
|
+
if (m.jsonrpc !== JSONRPC) throw new ProtocolError('rpc message is not jsonrpc 2.0')
|
|
115
|
+
const hasId = typeof m.id === 'number' || typeof m.id === 'string'
|
|
116
|
+
if ('method' in m) {
|
|
117
|
+
if (typeof m.method !== 'string') throw new ProtocolError('rpc method must be a string')
|
|
118
|
+
if ('id' in m && !hasId) throw new ProtocolError('rpc id must be a number or string')
|
|
119
|
+
return value as unknown as RpcRequest | RpcNotification
|
|
120
|
+
}
|
|
121
|
+
if (!hasId) throw new ProtocolError('rpc response needs a numeric or string id')
|
|
122
|
+
return value as unknown as RpcResponse
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---------- 错误 ----------
|
|
126
|
+
|
|
127
|
+
const CODE_BY_NUMBER = new Map<number, string>(
|
|
128
|
+
Object.entries(ERROR_CODE_NUMBERS).map(([code, n]) => [n, code]),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
/** 远端返回(或本地构造)的 JSON-RPC 错误。`code` 是稳定的 snake_case,客户端据此 i18n。 */
|
|
132
|
+
export class AnyconRpcError extends Error {
|
|
133
|
+
override readonly name: string = 'AnyconRpcError'
|
|
134
|
+
/** JSON-RPC 数字 code。 */
|
|
135
|
+
readonly rpcCode: number
|
|
136
|
+
readonly data: RpcErrorData | null
|
|
137
|
+
|
|
138
|
+
constructor(rpcCode: number, message: string, data?: RpcErrorData | null) {
|
|
139
|
+
super(message)
|
|
140
|
+
this.rpcCode = rpcCode
|
|
141
|
+
this.data = data ?? null
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** 稳定错误码:优先 `data.code`,没有就按数字 code 反查,再不行是 `internal`。 */
|
|
145
|
+
get code(): ErrorCode | (string & {}) {
|
|
146
|
+
return this.data?.code ?? CODE_BY_NUMBER.get(this.rpcCode) ?? 'internal'
|
|
147
|
+
}
|
|
148
|
+
/** `invalid_settings` 时指出出错字段。 */
|
|
149
|
+
get field(): string | undefined {
|
|
150
|
+
return this.data?.field ?? undefined
|
|
151
|
+
}
|
|
152
|
+
get reason(): string | undefined {
|
|
153
|
+
return this.data?.reason ?? undefined
|
|
154
|
+
}
|
|
155
|
+
get retryAfterMs(): number | undefined {
|
|
156
|
+
return this.data?.retry_after_ms ?? undefined
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
static fromWire(err: RpcError): AnyconRpcError {
|
|
160
|
+
return new AnyconRpcError(err.code, err.message, err.data)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** 构造稳定码对应的错误(daemon 侧请求处理器、测试用)。 */
|
|
164
|
+
static of(code: ErrorCode, message: string, extra?: Partial<RpcErrorData>): AnyconRpcError {
|
|
165
|
+
return new AnyconRpcError(ERROR_CODE_NUMBERS[code], message, { code, ...extra })
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
toWire(): RpcError {
|
|
169
|
+
const wire: RpcError = { code: this.rpcCode, message: this.message }
|
|
170
|
+
if (this.data) wire.data = this.data
|
|
171
|
+
return wire
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** 请求超时。 */
|
|
176
|
+
export class RpcTimeoutError extends Error {
|
|
177
|
+
override readonly name = 'RpcTimeoutError'
|
|
178
|
+
constructor(
|
|
179
|
+
readonly method: string,
|
|
180
|
+
readonly timeoutMs: number,
|
|
181
|
+
) {
|
|
182
|
+
super(`rpc ${method} timed out after ${timeoutMs}ms`)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ---------- RpcPeer ----------
|
|
187
|
+
|
|
188
|
+
export interface RpcPeerOptions {
|
|
189
|
+
/** 发出一条已编码的 JSON-RPC 消息(上层负责加密 + 装信封)。同步抛错会让对应 `request()` 拒绝。 */
|
|
190
|
+
send: (bytes: Uint8Array) => void
|
|
191
|
+
/** `request()` 默认超时,毫秒。默认 30 秒。 */
|
|
192
|
+
timeoutMs?: number
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export interface RequestOptions {
|
|
196
|
+
timeoutMs?: number
|
|
197
|
+
signal?: AbortSignal
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
interface Pending {
|
|
201
|
+
method: string
|
|
202
|
+
resolve: (value: unknown) => void
|
|
203
|
+
reject: (reason: unknown) => void
|
|
204
|
+
timer: ReturnType<typeof setTimeout> | undefined
|
|
205
|
+
cleanup: () => void
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
type NotificationHandler = (params: unknown, method: string) => void
|
|
209
|
+
type RequestHandler = (params: unknown) => unknown | Promise<unknown>
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 与传输无关的 JSON-RPC 端点。`send` 注入,入站字节喂给 `handle()`。
|
|
213
|
+
* 请求 id 是递增整数;响应按 id 关联;通知按方法名分发(`'*'` 收所有)。
|
|
214
|
+
*/
|
|
215
|
+
export class RpcPeer {
|
|
216
|
+
readonly #send: (bytes: Uint8Array) => void
|
|
217
|
+
readonly #defaultTimeoutMs: number
|
|
218
|
+
readonly #pending = new Map<RpcId, Pending>()
|
|
219
|
+
readonly #notificationHandlers = new Map<string, Set<NotificationHandler>>()
|
|
220
|
+
readonly #requestHandlers = new Map<string, RequestHandler>()
|
|
221
|
+
#nextId = 1
|
|
222
|
+
|
|
223
|
+
constructor(options: RpcPeerOptions) {
|
|
224
|
+
this.#send = options.send
|
|
225
|
+
this.#defaultTimeoutMs = options.timeoutMs ?? 30_000
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** 未答复的请求数。 */
|
|
229
|
+
get pending(): number {
|
|
230
|
+
return this.#pending.size
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
request<T = unknown>(method: string, params?: unknown, options: RequestOptions = {}): Promise<T> {
|
|
234
|
+
const id = this.#nextId++
|
|
235
|
+
const timeoutMs = options.timeoutMs ?? this.#defaultTimeoutMs
|
|
236
|
+
return new Promise<T>((resolve, reject) => {
|
|
237
|
+
const { signal } = options
|
|
238
|
+
if (signal?.aborted) {
|
|
239
|
+
reject(signal.reason ?? new Error('aborted'))
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
const entry: Pending = {
|
|
243
|
+
method,
|
|
244
|
+
resolve: resolve as (value: unknown) => void,
|
|
245
|
+
reject,
|
|
246
|
+
timer: undefined,
|
|
247
|
+
cleanup: () => {
|
|
248
|
+
if (entry.timer !== undefined) clearTimeout(entry.timer)
|
|
249
|
+
signal?.removeEventListener('abort', onAbort)
|
|
250
|
+
this.#pending.delete(id)
|
|
251
|
+
},
|
|
252
|
+
}
|
|
253
|
+
const onAbort = () => {
|
|
254
|
+
entry.cleanup()
|
|
255
|
+
reject(signal?.reason ?? new Error('aborted'))
|
|
256
|
+
}
|
|
257
|
+
if (timeoutMs > 0 && Number.isFinite(timeoutMs)) {
|
|
258
|
+
entry.timer = setTimeout(() => {
|
|
259
|
+
entry.cleanup()
|
|
260
|
+
reject(new RpcTimeoutError(method, timeoutMs))
|
|
261
|
+
}, timeoutMs)
|
|
262
|
+
}
|
|
263
|
+
signal?.addEventListener('abort', onAbort, { once: true })
|
|
264
|
+
this.#pending.set(id, entry)
|
|
265
|
+
try {
|
|
266
|
+
this.#send(encodeRpc(makeRequest(id, method, params ?? {})))
|
|
267
|
+
} catch (err) {
|
|
268
|
+
entry.cleanup()
|
|
269
|
+
reject(err)
|
|
270
|
+
}
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
notify(method: string, params?: unknown): void {
|
|
275
|
+
this.#send(encodeRpc(makeNotification(method, params ?? {})))
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** 订阅通知。`method = '*'` 收全部。返回取消订阅函数。 */
|
|
279
|
+
onNotification<T = unknown>(
|
|
280
|
+
method: string,
|
|
281
|
+
handler: (params: T, method: string) => void,
|
|
282
|
+
): () => void {
|
|
283
|
+
let set = this.#notificationHandlers.get(method)
|
|
284
|
+
if (!set) {
|
|
285
|
+
set = new Set()
|
|
286
|
+
this.#notificationHandlers.set(method, set)
|
|
287
|
+
}
|
|
288
|
+
const h = handler as NotificationHandler
|
|
289
|
+
set.add(h)
|
|
290
|
+
return () => {
|
|
291
|
+
set.delete(h)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** 注册对端发来的请求的处理器(目前方向是 D→C,留给以后)。每个方法至多一个。 */
|
|
296
|
+
onRequest<T = unknown, R = unknown>(
|
|
297
|
+
method: string,
|
|
298
|
+
handler: (params: T) => R | Promise<R>,
|
|
299
|
+
): () => void {
|
|
300
|
+
this.#requestHandlers.set(method, handler as RequestHandler)
|
|
301
|
+
return () => {
|
|
302
|
+
if (this.#requestHandlers.get(method) === (handler as RequestHandler)) {
|
|
303
|
+
this.#requestHandlers.delete(method)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** 处理一条入站消息。解析失败抛 `ProtocolError`;处理器内部的错误不会向外抛。 */
|
|
309
|
+
handle(bytes: Uint8Array): void {
|
|
310
|
+
const msg = decodeRpc(bytes)
|
|
311
|
+
if (isResponse(msg)) {
|
|
312
|
+
this.#onResponse(msg)
|
|
313
|
+
} else if (isRequest(msg)) {
|
|
314
|
+
void this.#onRequest(msg)
|
|
315
|
+
} else {
|
|
316
|
+
this.#onNotification(msg)
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** 拒绝所有未答复请求(连接断开时调用)。 */
|
|
321
|
+
failAll(error: Error): void {
|
|
322
|
+
const entries = Array.from(this.#pending.values())
|
|
323
|
+
for (const entry of entries) {
|
|
324
|
+
entry.cleanup()
|
|
325
|
+
entry.reject(error)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
#onResponse(msg: RpcResponse): void {
|
|
330
|
+
const entry = this.#pending.get(msg.id)
|
|
331
|
+
if (!entry) return
|
|
332
|
+
entry.cleanup()
|
|
333
|
+
if (msg.error) entry.reject(AnyconRpcError.fromWire(msg.error))
|
|
334
|
+
else entry.resolve(msg.result)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
#onNotification(msg: RpcNotification): void {
|
|
338
|
+
for (const key of [msg.method, '*']) {
|
|
339
|
+
// 先拷贝:订阅者可能在回调里取消订阅。
|
|
340
|
+
const handlers = Array.from(this.#notificationHandlers.get(key) ?? [])
|
|
341
|
+
for (const handler of handlers) {
|
|
342
|
+
try {
|
|
343
|
+
handler(msg.params, msg.method)
|
|
344
|
+
} catch {
|
|
345
|
+
// 订阅者的异常不能影响其他订阅者和连接。
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async #onRequest(msg: RpcRequest): Promise<void> {
|
|
352
|
+
const handler = this.#requestHandlers.get(msg.method)
|
|
353
|
+
let reply: RpcResponse
|
|
354
|
+
if (!handler) {
|
|
355
|
+
reply = makeError(msg.id, {
|
|
356
|
+
code: -32601,
|
|
357
|
+
message: `method not found: ${msg.method}`,
|
|
358
|
+
data: { code: 'unsupported' },
|
|
359
|
+
})
|
|
360
|
+
} else {
|
|
361
|
+
try {
|
|
362
|
+
reply = makeResult(msg.id, (await handler(msg.params)) ?? null)
|
|
363
|
+
} catch (err) {
|
|
364
|
+
const rpcErr =
|
|
365
|
+
err instanceof AnyconRpcError
|
|
366
|
+
? err
|
|
367
|
+
: AnyconRpcError.of('internal', err instanceof Error ? err.message : String(err))
|
|
368
|
+
reply = makeError(msg.id, rpcErr.toWire())
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
try {
|
|
372
|
+
this.#send(encodeRpc(reply))
|
|
373
|
+
} catch {
|
|
374
|
+
// 连接已断,对端也拿不到答复了。
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|