mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
feat: simplify Chord service slots
This commit is contained in:
parent
1bf0de2144
commit
b1e9b9ea8f
2 changed files with 66 additions and 64 deletions
|
|
@ -18,9 +18,16 @@ import type {
|
|||
|
||||
interface FacetServiceReference {
|
||||
readonly serviceId: string;
|
||||
readonly service: Service<unknown>;
|
||||
readonly mode: ServiceMode;
|
||||
}
|
||||
|
||||
interface ExternalService {
|
||||
readonly service: Service<unknown>;
|
||||
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<string, ServiceHandle>();
|
||||
readonly #singletonServices = new Map<string, Service<unknown>>();
|
||||
readonly #keyedSources = new Map<string, KeyedServiceSource>();
|
||||
|
||||
constructor(assertAccess: () => void) {
|
||||
this.#assertAccess = assertAccess;
|
||||
}
|
||||
|
||||
use<T>(service: Service<T>): T {
|
||||
getSingleton<T>(service: Service<T>): 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<unknown>);
|
||||
}
|
||||
return handle.proxy as T;
|
||||
}
|
||||
|
||||
observe<T>(service: Service<T>, handler: (service: T, context: Context) => void | Promise<void>): () => 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<T>(service: Service<T>, handler: (service: T, context: Context) => void | Promise<void>): 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<string, FacetRuntime>();
|
||||
readonly #handles: FacetServiceHandles;
|
||||
readonly #serviceSlots: HostServiceSlots;
|
||||
readonly #connectionBindings = new Map<FacetConnection, RemoteServices>();
|
||||
#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: <T>(service: Service<T>): T => {
|
||||
lifecycle.assertSettingUp("acquire services");
|
||||
recordServiceReference(runtime.requires, service, "singleton");
|
||||
return this.#handles.use(service);
|
||||
return this.#serviceSlots.getSingleton(service);
|
||||
},
|
||||
observe: <T>(service: Service<T>, handler: (service: T, context: Context) => void | Promise<void>): 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: <T>(initial: T) => {
|
||||
lifecycle.assertRunning("create replicated state");
|
||||
|
|
@ -573,9 +567,7 @@ export class FacetKernel {
|
|||
};
|
||||
}
|
||||
|
||||
async #resolveExternalServices(
|
||||
records: readonly FacetShape[],
|
||||
): Promise<ReadonlyMap<string, { readonly mode: ServiceMode; readonly connection: FacetConnection }>> {
|
||||
async #resolveExternalServices(records: readonly FacetShape[]): Promise<ReadonlyMap<string, ExternalService>> {
|
||||
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<string, { readonly mode: ServiceMode; readonly connection: FacetConnection }>();
|
||||
const external = new Map<string, ExternalService>();
|
||||
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<FacetConnection, string[]>();
|
||||
|
|
@ -654,21 +648,22 @@ export class FacetKernel {
|
|||
}
|
||||
}
|
||||
|
||||
#bindServices(
|
||||
externalServices: ReadonlyMap<string, { readonly mode: ServiceMode; readonly connection: FacetConnection }>,
|
||||
): void {
|
||||
#bindServices(externalServices: ReadonlyMap<string, ExternalService>): 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<unknown>, mode });
|
||||
}
|
||||
|
||||
function sameFacetShape(left: FacetShape, right: FacetShape): boolean {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
export class ServiceHandle {
|
||||
readonly #serviceId: string;
|
||||
readonly #assertAccess: () => void;
|
||||
readonly #methods = new Map<PropertyKey, unknown>();
|
||||
readonly #members = new Map<PropertyKey, unknown>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue