返回 CodeWhale
owned.ts
根目录 / crates / tui / extension-host / src / shims / owned.ts
1 /**
2 * Owned registrations: the host half of `registry/register` for one kind.
3 *
4 * Every shim that contributes something to the core (tools, commands) follows
5 * the same life cycle: propose it with `registry/register`, keep the handle
6 * the core admitted, undo exactly that handle when the plugin's effect is
7 * disposed, and report a refusal so activation fails with the core's reason.
8 * The Rust side is the authority (`OwnerRegistry`); this only keeps the
9 * host's own index by handle so a later `tool/call` or `command/run` can find
10 * the definition.
11 */
12 import type { OwnerRef, EntryRef } from '../protocol.ts'
13 import type { RpcPeer } from '../rpc.ts'
14
15 /** What an owner record must carry for registrations to be tracked on it. */
16 export interface OwnerBase {
17 ref: OwnerRef
18 scope?: EntryRef
19 state: 'activating' | 'active' | 'failed' | 'disposed'
20 /** In-flight `registry/register` requests, awaited before activation acks. */
21 pendingRegistrations: Set<Promise<void>>
22 refusals: string[]
23 }
24
25 /** One registration a plugin made, from this host's side. */
26 export interface OwnedEntry<O extends OwnerBase> {
27 owner: O
28 name: string
29 /** Set when the core admits it. */
30 handle?: number
31 disposed: boolean
32 }
33
34 export type RegisterSpec = Record<string, unknown>
35
36 function describeError(error: unknown): string {
37 return error instanceof Error ? `${error.name}: ${error.message}` : String(error)
38 }
39
40 export class OwnedRegistrations<O extends OwnerBase, T extends OwnedEntry<O>> {
41 /** Admitted entries by core handle. */
42 readonly byHandle = new Map<number, T>()
43
44 private readonly rpc: RpcPeer
45 private readonly kind: 'tool' | 'command' | 'hook' | 'prompt_section' | 'prompt_template' | 'skill_root' | 'shell_hook' | 'mcp_server'
46 /** The owner's own index of this kind, for the leak report at deactivation. */
47 private readonly ownedBy: (owner: O) => Map<number, T>
48 private readonly warn: (message: string, owner: O) => void
49
50 constructor(
51 rpc: RpcPeer,
52 kind: 'tool' | 'command' | 'hook' | 'prompt_section' | 'prompt_template' | 'skill_root' | 'shell_hook' | 'mcp_server',
53 ownedBy: (owner: O) => Map<number, T>,
54 warn: (message: string, owner: O) => void,
55 ) {
56 this.rpc = rpc
57 this.kind = kind
58 this.ownedBy = ownedBy
59 this.warn = warn
60 }
61
62 /** Called inside the owner's effect; returns the effect's cleanup. */
63 add(entry: T, spec: RegisterSpec): () => void {
64 const owner = entry.owner
65 if (owner.state!=='activating' && owner.state!=='active') throw new Error('registration owner was disposed')
66 const registration: Promise<void> = this.rpc
67 .request('registry/register', { owner: owner.ref, ...(owner.scope ? {scope:owner.scope} : {}), kind: this.kind, spec })
68 .then(
69 (result: any) => {
70 if (typeof result?.handle === 'number') {
71 entry.handle = result.handle
72 if (entry.disposed || (owner.state!=='activating' && owner.state!=='active')) {
73 return this.unregister(entry)
74 }
75 this.ownedBy(owner).set(result.handle, entry)
76 this.byHandle.set(result.handle, entry)
77 } else {
78 const reason = typeof result?.refused === 'string' ? result.refused : 'refused without a reason'
79 owner.refusals.push(`${this.kind} \`${entry.name}\` refused: ${reason}`)
80 if (owner.state === 'active') this.warn(`${this.kind} \`${entry.name}\` refused: ${reason}`, owner)
81 }
82 },
83 (error: unknown) => {
84 owner.refusals.push(`${this.kind} \`${entry.name}\` registration failed: ${describeError(error)}`)
85 },
86 )
87 .finally(() => owner.pendingRegistrations.delete(registration))
88 owner.pendingRegistrations.add(registration)
89 return () => {
90 if (entry.disposed) return
91 entry.disposed = true
92 if (entry.handle !== undefined) void this.unregister(entry)
93 }
94 }
95
96 /** Undo exactly this entry. The core revokes first, so a failure here is expected after revocation. */
97 private unregister(entry: T): Promise<void> {
98 const handle = entry.handle!
99 const owner = entry.owner
100 this.ownedBy(owner).delete(handle)
101 this.byHandle.delete(handle)
102 // Rollback is part of owner quiescence, including an admission response
103 // arriving after its fiber failed. No activation/disposal ack precedes it.
104 const cleanup: Promise<void> = this.rpc
105 .request('registry/unregister', { owner: owner.ref, handle })
106 .then(() => undefined, () => undefined) // Core may have revoked first.
107 .finally(() => owner.pendingRegistrations.delete(cleanup))
108 owner.pendingRegistrations.add(cleanup)
109 return cleanup
110 }
111
112 /** Drop the host's index of everything `owner` registered (after its teardown). */
113 forget(owner: O) {
114 for (const entry of this.ownedBy(owner).values()) this.byHandle.delete(entry.handle!)
115 }
116 }
117
117 lines TYPESCRIPT