diff --git a/packages/chord/src/facets/host.ts b/packages/chord/src/facets/host.ts index 5be57edf9..16d3d85a6 100644 --- a/packages/chord/src/facets/host.ts +++ b/packages/chord/src/facets/host.ts @@ -18,9 +18,16 @@ import type { interface FacetServiceReference { readonly serviceId: string; + readonly service: Service; readonly mode: ServiceMode; } +interface ExternalService { + readonly service: Service; + readonly mode: ServiceMode; + readonly connection: FacetConnection; +} + interface FacetShape { facetId: string; requires: readonly FacetServiceReference[]; @@ -201,50 +208,38 @@ class LocalKeyedServiceRegistry implements KeyedServiceSource { } } -class FacetServiceHandles { +class HostServiceSlots { readonly #assertAccess: () => void; readonly #singletons = new Map(); - readonly #singletonServices = new Map>(); readonly #keyedSources = new Map(); constructor(assertAccess: () => void) { this.#assertAccess = assertAccess; } - use(service: Service): T { + getSingleton(service: Service): T { let handle = this.#singletons.get(service.id); if (handle === undefined) { handle = new ServiceHandle(service.id, this.#assertAccess); this.#singletons.set(service.id, handle); - this.#singletonServices.set(service.id, service as Service); } return handle.proxy as T; } - observe(service: Service, handler: (service: T, context: Context) => void | Promise): () => Disposal { - return () => { - const source = this.#keyedSources.get(service.id); - if (source === undefined) throw new Error(`Service ${service.id} is disconnected`); - return source.observe(service, handler); - }; + observe(service: Service, handler: (service: T, context: Context) => void | Promise): Disposal { + const source = this.#keyedSources.get(service.id); + if (source === undefined) throw new Error(`Service ${service.id} is disconnected`); + return source.observe(service, handler); } - bindSingleton(serviceId: string, implementation: object): void { - this.#singletons.get(serviceId)?.bind(implementation); + bindSingleton(serviceId: string, target: object): void { + this.#singletons.get(serviceId)?.bind(target); } unbindSingleton(serviceId: string): void { this.#singletons.get(serviceId)?.unbind(); } - bindRemoteSingleton(serviceId: string, services: RemoteServices): void { - const handle = this.#singletons.get(serviceId); - const service = this.#singletonServices.get(serviceId); - if (handle === undefined || service === undefined) - throw new Error(`Service ${serviceId} has no singleton handle`); - handle.bind(services.use(service) as object, true); - } - bindKeyed(serviceId: string, services: KeyedServiceSource): void { this.#keyedSources.set(serviceId, services); } @@ -252,7 +247,6 @@ class FacetServiceHandles { dispose(): void { for (const handle of this.#singletons.values()) handle.unbind(); this.#singletons.clear(); - this.#singletonServices.clear(); this.#keyedSources.clear(); } } @@ -314,7 +308,7 @@ export class FacetKernel { readonly #connections: readonly FacetConnection[]; readonly #onError: (error: Error) => void; readonly #facets = new Map(); - readonly #handles: FacetServiceHandles; + readonly #serviceSlots: HostServiceSlots; readonly #connectionBindings = new Map(); #activationOrder: readonly string[] = []; #provider: RemoteServiceProvider | undefined; @@ -328,7 +322,7 @@ export class FacetKernel { this.#initialFacets = options.facets; this.#connections = options.connections ?? []; this.#onError = options.onError ?? (() => {}); - this.#handles = new FacetServiceHandles(() => this.#assertServiceAccess()); + this.#serviceSlots = new HostServiceSlots(() => this.#assertServiceAccess()); } get provider(): RemoteServiceProvider { @@ -388,7 +382,7 @@ export class FacetKernel { cleanupErrors.push(cleanupError); } cleanupErrors.push(...(await this.#disposeConnectionBindings())); - this.#handles.dispose(); + this.#serviceSlots.dispose(); this.#provider?.dispose(); this.#phase = "dead"; if (cleanupErrors.length > 0) { @@ -442,7 +436,7 @@ export class FacetKernel { for (const candidate of candidates) { for (const provision of candidate.provisions) { if (provision.kind !== "singleton") continue; - this.#handles.unbindSingleton(provision.service.id); + this.#serviceSlots.unbindSingleton(provision.service.id); if (!provision.service.local) provision.withdraw(this.provider); } } @@ -485,7 +479,7 @@ export class FacetKernel { await candidate.lifecycle.activate(); for (const provision of candidate.provisions) { if (provision.kind !== "singleton") continue; - this.#handles.bindSingleton(provision.service.id, provision.implementation); + this.#serviceSlots.bindSingleton(provision.service.id, provision.implementation); if (!provision.service.local) provision.replace(this.provider); } } @@ -505,7 +499,7 @@ export class FacetKernel { errors.push(error); } errors.push(...(await this.#disposeConnectionBindings())); - this.#handles.dispose(); + this.#serviceSlots.dispose(); this.#provider?.dispose(); this.#phase = "dead"; if (errors.length === 1) throw errors[0]; @@ -556,12 +550,12 @@ export class FacetKernel { use: (service: Service): T => { lifecycle.assertSettingUp("acquire services"); recordServiceReference(runtime.requires, service, "singleton"); - return this.#handles.use(service); + return this.#serviceSlots.getSingleton(service); }, observe: (service: Service, handler: (service: T, context: Context) => void | Promise): void => { lifecycle.assertSettingUp("observe services"); recordServiceReference(runtime.requires, service, "keyed"); - lifecycle.observe(this.#handles.observe(service, handler)); + lifecycle.observe(() => this.#serviceSlots.observe(service, handler)); }, replicatedState: (initial: T) => { lifecycle.assertRunning("create replicated state"); @@ -573,9 +567,7 @@ export class FacetKernel { }; } - async #resolveExternalServices( - records: readonly FacetShape[], - ): Promise> { + async #resolveExternalServices(records: readonly FacetShape[]): Promise> { const catalogues = await Promise.all( this.#connections.map(async (connection) => ({ connection, @@ -593,7 +585,7 @@ export class FacetKernel { } const local = new Set(records.flatMap(({ provides }) => provides.map(({ serviceId }) => serviceId))); - const external = new Map(); + const external = new Map(); for (const { requires } of records) { for (const requirement of requires) { if (local.has(requirement.serviceId) || external.has(requirement.serviceId)) continue; @@ -607,7 +599,9 @@ export class FacetKernel { } if (deferred.length === 1) provider = { mode: requirement.mode, connection: deferred[0]! }; } - if (provider !== undefined) external.set(requirement.serviceId, provider); + if (provider !== undefined) { + external.set(requirement.serviceId, { ...provider, service: requirement.service }); + } } } const serviceIdsByConnection = new Map(); @@ -654,21 +648,22 @@ export class FacetKernel { } } - #bindServices( - externalServices: ReadonlyMap, - ): void { + #bindServices(externalServices: ReadonlyMap): void { for (const provision of this.#provisions()) { if (provision.kind === "singleton") { - this.#handles.bindSingleton(provision.service.id, provision.implementation); + this.#serviceSlots.bindSingleton(provision.service.id, provision.implementation); } else { - this.#handles.bindKeyed(provision.service.id, this.#localKeyedRegistry); + this.#serviceSlots.bindKeyed(provision.service.id, this.#localKeyedRegistry); } } - for (const [serviceId, { mode, connection }] of externalServices) { + for (const [serviceId, { service, mode, connection }] of externalServices) { const services = this.#connectionBindings.get(connection); if (services === undefined) throw new Error(`Service connection for ${serviceId} is not open`); - if (mode === "singleton") this.#handles.bindRemoteSingleton(serviceId, services); - else this.#handles.bindKeyed(serviceId, services); + if (mode === "singleton") { + this.#serviceSlots.bindSingleton(serviceId, services.use(service) as object); + } else { + this.#serviceSlots.bindKeyed(serviceId, services); + } } } @@ -792,7 +787,7 @@ function recordServiceReference( mode: ServiceMode, ): void { if (target.some((reference) => reference.serviceId === service.id && reference.mode === mode)) return; - target.push({ serviceId: service.id, mode }); + target.push({ serviceId: service.id, service: service as Service, mode }); } function sameFacetShape(left: FacetShape, right: FacetShape): boolean { diff --git a/packages/chord/src/services/handle.ts b/packages/chord/src/services/handle.ts index 0ee6f0ebd..9fd93cede 100644 --- a/packages/chord/src/services/handle.ts +++ b/packages/chord/src/services/handle.ts @@ -1,10 +1,9 @@ export class ServiceHandle { readonly #serviceId: string; readonly #assertAccess: () => void; - readonly #methods = new Map(); + readonly #members = new Map(); readonly proxy: object; #implementation: object | undefined; - #remote = false; constructor(serviceId: string, assertAccess: () => void) { this.#serviceId = serviceId; @@ -13,38 +12,46 @@ export class ServiceHandle { {}, { get: (_target, property) => { - this.#assertAccess(); - if (this.#implementation === undefined) throw new Error(`Service ${this.#serviceId} is disconnected`); - const value: unknown = Reflect.get(this.#implementation, property, this.#implementation); - if (this.#remote || typeof value !== "function") return value; - let method = this.#methods.get(property); - if (method === undefined) { - method = (...args: unknown[]) => { - this.#assertAccess(); - if (this.#implementation === undefined) { - throw new Error(`Service ${this.#serviceId} is disconnected`); - } - const current: unknown = Reflect.get(this.#implementation, property, this.#implementation); + const value = this.#getMember(property); + if (typeof value !== "function") return value; + let member = this.#members.get(property); + if (member === undefined) { + const invoke = (...args: unknown[]): unknown => { + const implementation = this.#implementation; + const current = this.#getMember(property); if (typeof current !== "function") { - throw new TypeError(`Service ${this.#serviceId}.${String(property)} is not a method`); + throw new TypeError(`Service ${this.#serviceId}.${String(property)} is not callable`); } - return Reflect.apply(current, this.#implementation, args); + return Reflect.apply(current, implementation, args); }; - this.#methods.set(property, method); + member = new Proxy(invoke, { + get: (_target, memberProperty) => { + const current = this.#getMember(property); + if (typeof current !== "function") { + throw new TypeError(`Service ${this.#serviceId}.${String(property)} is not callable`); + } + return Reflect.get(current, memberProperty, current); + }, + }); + this.#members.set(property, member); } - return method; + return member; }, }, ); } - bind(implementation: object, remote = false): void { + bind(implementation: object): void { this.#implementation = implementation; - this.#remote = remote; } unbind(): void { this.#implementation = undefined; - this.#remote = false; + } + + #getMember(property: PropertyKey): unknown { + this.#assertAccess(); + if (this.#implementation === undefined) throw new Error(`Service ${this.#serviceId} is disconnected`); + return Reflect.get(this.#implementation, property, this.#implementation); } }