@@ -16,6 +16,18 @@ import { deepMergeOptionSets, expandResolveFrom, importServicePackage, satisfies
1616
1717const debug = createDebug ( 'devframe:services' )
1818
19+ /**
20+ * Per-RPC-host registry of the services already installed against it, keyed
21+ * by the RPC host object. A single wire service can be declared from more
22+ * than one devframe context that shares one RPC host (e.g. a kit that mounts
23+ * a definition alongside another context, so both iterate `def.services`).
24+ * The per-instance `installed` guard can't see across those sibling hosts, so
25+ * the second context would re-run the service factory and re-register its RPC,
26+ * hitting DF0021. Sharing the registry by RPC host makes the first install win
27+ * across every context on that host.
28+ */
29+ const installedByRpcHost = new WeakMap < object , Map < string , unknown > > ( )
30+
1931interface PendingServiceEntry {
2032 input : DevframeServiceInput
2133 resolveFrom ?: string | null
@@ -72,11 +84,29 @@ export class DevframeServicesHostImpl implements DevframeServicesHost {
7284 private services = new Map < string , unknown > ( )
7385 private listeners = new Map < string , Set < ( service : unknown ) => void > > ( )
7486 private pending = new Map < string , PendingServiceEntry [ ] > ( )
75- private installed = new Map < string , unknown > ( )
87+ private localInstalled = new Map < string , unknown > ( )
7688 private readyPromise : Promise < void > | undefined
7789
7890 constructor ( private context ?: DevframeNodeContext ) { }
7991
92+ /**
93+ * The install-dedup registry, shared across every services host that shares
94+ * this host's RPC host so the first install of a package wins across sibling
95+ * contexts. Falls back to a per-instance map when there is no RPC host (a
96+ * context-less host only exercises the in-process `provide`/`get` tier).
97+ */
98+ private get installed ( ) : Map < string , unknown > {
99+ const rpc = this . context ?. rpc as object | undefined
100+ if ( ! rpc )
101+ return this . localInstalled
102+ let registry = installedByRpcHost . get ( rpc )
103+ if ( ! registry ) {
104+ registry = new Map ( )
105+ installedByRpcHost . set ( rpc , registry )
106+ }
107+ return registry
108+ }
109+
80110 provide < ID extends DevframeServiceId > ( id : ID , service : DevframeServiceOf < ID > ) : ( ) => void {
81111 const key = id as string
82112 if ( this . services . has ( key ) )
@@ -190,8 +220,17 @@ export class DevframeServicesHostImpl implements DevframeServicesHost {
190220 private async installPackage ( pkg : string , entries : PendingServiceEntry [ ] ) : Promise < unknown > {
191221 // Dedup: first installation wins; a later install's options are ignored.
192222 if ( this . installed . has ( pkg ) ) {
193- diagnostics . DF0066 ( { package : pkg } )
194- return this . installed . get ( pkg )
223+ const api = this . installed . get ( pkg )
224+ // A sibling context sharing this RPC host already constructed the
225+ // service. Expose the cached API here too, but skip re-running the
226+ // factory (which would re-register its RPC and hit DF0021). Only a
227+ // re-install on the same host (it already provides it) is the noisy
228+ // duplicate DF0066 warns about.
229+ if ( this . services . has ( pkg ) )
230+ diagnostics . DF0066 ( { package : pkg } )
231+ else
232+ this . provide ( pkg , api as DevframeServiceOf < string > )
233+ return api
195234 }
196235
197236 const definitions = entries . filter ( entry => isServiceDefinition ( entry . input ) )
@@ -211,7 +250,7 @@ export class DevframeServicesHostImpl implements DevframeServicesHost {
211250 debug ( 'installing service %s@%s (scope %s)' , def . package , def . version , def . scope )
212251 const scoped = this . context . scope ( def . scope )
213252 const api = await def . setup ( scoped , options === undefined ? { } : { options } )
214- this . installed . set ( def . package , api )
253+ this . installed . set ( pkg , api )
215254 this . provide ( def . package , api as DevframeServiceOf < string > )
216255 await this . advertise ( def )
217256 return api
0 commit comments