@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.
Files changed (49) hide show
  1. package/LICENSE +176 -0
  2. package/README.md +146 -0
  3. package/README.zh-CN.md +118 -0
  4. package/dist/constants.d.ts +151 -0
  5. package/dist/constants.d.ts.map +1 -0
  6. package/dist/constants.js +159 -0
  7. package/dist/constants.js.map +1 -0
  8. package/dist/crypto.d.ts +202 -0
  9. package/dist/crypto.d.ts.map +1 -0
  10. package/dist/crypto.js +218 -0
  11. package/dist/crypto.js.map +1 -0
  12. package/dist/envelope.d.ts +105 -0
  13. package/dist/envelope.d.ts.map +1 -0
  14. package/dist/envelope.js +390 -0
  15. package/dist/envelope.js.map +1 -0
  16. package/dist/errors.d.ts +5 -0
  17. package/dist/errors.d.ts.map +1 -0
  18. package/dist/errors.js +5 -0
  19. package/dist/errors.js.map +1 -0
  20. package/dist/generated.d.ts +2007 -0
  21. package/dist/generated.d.ts.map +1 -0
  22. package/dist/generated.js +4 -0
  23. package/dist/generated.js.map +1 -0
  24. package/dist/index.d.ts +17 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +16 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/method-types.d.ts +293 -0
  29. package/dist/method-types.d.ts.map +1 -0
  30. package/dist/method-types.js +2 -0
  31. package/dist/method-types.js.map +1 -0
  32. package/dist/rpc.d.ts +70 -0
  33. package/dist/rpc.d.ts.map +1 -0
  34. package/dist/rpc.js +310 -0
  35. package/dist/rpc.js.map +1 -0
  36. package/package.json +62 -0
  37. package/src/constants.ts +173 -0
  38. package/src/crypto.ts +427 -0
  39. package/src/envelope.ts +469 -0
  40. package/src/errors.ts +4 -0
  41. package/src/generated.ts +2094 -0
  42. package/src/index.ts +18 -0
  43. package/src/method-types.ts +183 -0
  44. package/src/rpc.ts +377 -0
  45. package/wasm/THIRD-PARTY-LICENSES.txt +1957 -0
  46. package/wasm/anycon_crypto.d.ts +248 -0
  47. package/wasm/anycon_crypto.js +1250 -0
  48. package/wasm/anycon_crypto_bg.wasm +0 -0
  49. 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
+ }