UNPKG

@fjell/registry

Version:

Dependency injection and service location system for the Fjell ecosystem

698 lines (688 loc) 22.7 kB
// src/logger.ts import Logging from "@fjell/logging"; var LibLogger = Logging.getLogger("@fjell/registry"); var logger_default = LibLogger; // src/Instance.ts var logger = logger_default.get("Instance"); var createInstance = (registry, coordinate) => { logger.debug("createInstance", { coordinate, registry }); return { coordinate, registry }; }; var isInstance = (instance) => { return instance !== null && instance !== void 0 && instance.coordinate !== void 0 && instance.registry !== void 0; }; // src/Registry.ts import { createCoordinate } from "@fjell/core"; // src/RegistryStats.ts var RegistryStats = class { totalCalls = 0; // Map structure: ktaKey -> scopeKey -> clientKey -> count coordinateCalls = /* @__PURE__ */ new Map(); /** * Records a get() call for the specified coordinate and client */ recordGetCall(kta, scopes, client) { this.totalCalls++; const ktaKey = kta.join("."); const scopeKey = this.createScopeKey(scopes || []); const clientKey = this.createClientKey(client); if (!this.coordinateCalls.has(ktaKey)) { this.coordinateCalls.set(ktaKey, /* @__PURE__ */ new Map()); } const scopeMap = this.coordinateCalls.get(ktaKey); if (!scopeMap.has(scopeKey)) { scopeMap.set(scopeKey, /* @__PURE__ */ new Map()); } const clientMap = scopeMap.get(scopeKey); const currentCount = clientMap.get(clientKey) || 0; clientMap.set(clientKey, currentCount + 1); } /** * Gets the current statistics snapshot */ getStatistics() { const coordinateCallRecords = []; let serviceCalls = 0; let applicationCalls = 0; let unidentifiedCalls = 0; for (const [ktaKey, scopeMap] of this.coordinateCalls) { for (const [scopeKey, clientMap] of scopeMap) { const clientCalls = []; let totalCount = 0; for (const [clientKey, count] of clientMap) { const client = this.parseClientKey(clientKey); if (client !== null) { clientCalls.push({ client, count }); } totalCount += count; if (clientKey === "__no_client__") { unidentifiedCalls += count; } else if (typeof client === "string") { applicationCalls += count; } else if (client !== null) { serviceCalls += count; } } coordinateCallRecords.push({ kta: ktaKey.split("."), scopes: this.parseScopeKey(scopeKey), count: totalCount, clientCalls: [...clientCalls] // Return a copy }); } } return { totalGetCalls: this.totalCalls, coordinateCallRecords: [...coordinateCallRecords], // Return a copy clientSummary: { serviceCalls, applicationCalls, unidentifiedCalls } }; } /** * Gets call count for a specific coordinate combination */ getCallCount(kta, scopes) { const ktaKey = kta.join("."); const scopeKey = this.createScopeKey(scopes || []); const scopeMap = this.coordinateCalls.get(ktaKey); if (!scopeMap) return 0; const clientMap = scopeMap.get(scopeKey); if (!clientMap) return 0; let total = 0; for (const count of clientMap.values()) { total += count; } return total; } /** * Gets call count for a specific coordinate combination from a specific client */ getCallCountByClient(kta, scopes, client) { const ktaKey = kta.join("."); const scopeKey = this.createScopeKey(scopes || []); const clientKey = this.createClientKey(client); const scopeMap = this.coordinateCalls.get(ktaKey); if (!scopeMap) return 0; const clientMap = scopeMap.get(scopeKey); if (!clientMap) return 0; return clientMap.get(clientKey) || 0; } /** * Gets total calls for a specific kta (across all scopes) */ getTotalCallsForKta(kta) { const ktaKey = kta.join("."); const scopeMap = this.coordinateCalls.get(ktaKey); if (!scopeMap) return 0; let total = 0; for (const clientMap of scopeMap.values()) { for (const count of clientMap.values()) { total += count; } } return total; } /** * Gets all unique kta paths that have been called */ getCalledKtaPaths() { const ktaPaths = []; for (const ktaKey of this.coordinateCalls.keys()) { ktaPaths.push(ktaKey.split(".")); } return ktaPaths; } /** * Creates a normalized scope key from scopes array */ createScopeKey(scopes) { if (scopes.length === 0) return "__no_scopes__"; return [...scopes].sort().join(","); } /** * Parses a scope key back to scopes array */ parseScopeKey(scopeKey) { if (scopeKey === "__no_scopes__") return []; return scopeKey.split(","); } /** * Creates a normalized client key from client identifier */ createClientKey(client) { if (!client) return "__no_client__"; if (typeof client === "string") { return `app:${client}`; } const coordKey = `${client.coordinate.kta.join(".")};${this.createScopeKey(client.coordinate.scopes)}`; return `service:${client.registryType}:${coordKey}`; } /** * Parses a client key back to client identifier */ parseClientKey(clientKey) { if (clientKey === "__no_client__") return null; if (clientKey.startsWith("app:")) { return clientKey.substring(4); } if (clientKey.startsWith("service:")) { const parts = clientKey.substring(8).split(":"); if (parts.length !== 2) return null; const registryType = parts[0]; const coordParts = parts[1].split(";"); if (coordParts.length !== 2) return null; const kta = coordParts[0].split("."); const scopes = this.parseScopeKey(coordParts[1]); return { registryType, coordinate: { kta, scopes } }; } return null; } }; // src/Registry.ts var logger2 = logger_default.get("Registry"); var findScopedInstance = (scopedInstances, requestedScopes) => { if (!requestedScopes || requestedScopes.length === 0) { const firstInstance = scopedInstances[0]?.instance; if (!firstInstance) { throw new Error("No instances available"); } return firstInstance; } const matchingInstance = scopedInstances.find((scopedInstance) => { if (!scopedInstance.scopes) return false; return requestedScopes.every( (scope) => scopedInstance.scopes && scopedInstance.scopes.includes(scope) ); }); if (!matchingInstance) { const availableScopes = scopedInstances.map((si) => si.scopes?.join(", ") || "(no scopes)"); throw new Error( `No instance found matching scopes: ${requestedScopes.join(", ")}. Available scopes: ${availableScopes.join(" | ")}` ); } return matchingInstance.instance; }; var createRegistry = (type, registryHub) => { const instanceTree = {}; const registryStats = new RegistryStats(); const createProxiedRegistry = (callingCoordinate) => { const serviceClient = { registryType: type, coordinate: { kta: callingCoordinate.kta, scopes: callingCoordinate.scopes } }; return { ...registry, get: (kta, options) => { const clientToUse = options?.client || serviceClient; return registry.get(kta, { ...options, client: clientToUse }); } }; }; const createInstance2 = (kta, scopes, factory) => { logger2.debug(`Creating and registering instance for key path and scopes`, kta, scopes, `in registry type: ${type}`); const coordinate = createCoordinate(kta, scopes); const proxiedRegistry = createProxiedRegistry(coordinate); const instance = factory(coordinate, { registry: proxiedRegistry, registryHub }); if (!isInstance(instance)) { logger2.error("Factory returned invalid instance", { component: "registry", operation: "getOrCreateInstance", type, kta, returnedType: typeof instance, suggestion: "Ensure factory function returns a valid instance with operations property" }); throw new Error( `Factory did not return a valid instance for: ${kta.join(".")}. Expected instance with operations property, got: ${typeof instance}` ); } registerInternal(kta, instance, { scopes }); return instance; }; const registerInternal = (kta, instance, options) => { const keyPath = [...kta].reverse(); let currentLevel = instanceTree; logger2.debug(`Registering instance for key path and scopes`, keyPath, options?.scopes, `in registry type: ${type}`); if (!isInstance(instance)) { logger2.error("Attempting to register invalid instance", { component: "registry", operation: "registerInstance", type, kta, providedType: typeof instance, suggestion: "Ensure you are registering a valid instance with operations property, not a factory or other object" }); throw new Error( `Attempting to register a non-instance: ${kta.join(".")}. Expected instance with operations property, got: ${typeof instance}` ); } for (let i = 0; i < keyPath.length; i++) { const keyType = keyPath[i]; const isLeaf = i === keyPath.length - 1; if (!currentLevel[keyType]) { currentLevel[keyType] = { instances: [], children: isLeaf ? null : {} }; } if (isLeaf) { currentLevel[keyType].instances.push({ scopes: options?.scopes, instance }); } else { if (!currentLevel[keyType].children) { currentLevel[keyType].children = {}; } currentLevel = currentLevel[keyType].children; } } }; const register = (kta, instance, options) => { logger2.debug("Using deprecated register method. Consider using createInstance instead."); registerInternal(kta, instance, options); }; const get = (kta, options) => { registryStats.recordGetCall(kta, options?.scopes, options?.client); const keyPath = [...kta].reverse(); let currentLevel = instanceTree; for (let i = 0; i < keyPath.length; i++) { const keyType = keyPath[i]; const isLeaf = i === keyPath.length - 1; if (!currentLevel[keyType]) { throw new Error(`Instance not found for key path: ${kta.join(".")}, Missing key: ${keyType}`); } if (isLeaf) { const scopedInstances = currentLevel[keyType].instances; if (scopedInstances.length === 0) { throw new Error(`No instances registered for key path: ${kta.join(".")}`); } return findScopedInstance(scopedInstances, options?.scopes); } else { if (!currentLevel[keyType].children) { throw new Error(`Instance not found for key path: ${kta.join(".")}, No children for: ${keyType}`); } currentLevel = currentLevel[keyType].children; } } return null; }; const getCoordinates = () => { const coordinates = []; const traverseTree = (node) => { for (const keyType in node) { const treeNode = node[keyType]; for (const scopedInstance of treeNode.instances) { coordinates.push(scopedInstance.instance.coordinate); } if (treeNode.children) { traverseTree(treeNode.children); } } }; traverseTree(instanceTree); return coordinates; }; const getStatistics = () => { return registryStats.getStatistics(); }; const registry = { type, registryHub, createInstance: createInstance2, register, get, getCoordinates, getStatistics, instanceTree }; return registry; }; // src/errors/RegistryError.ts var RegistryError = class extends Error { registryType; context; constructor(message, registryType, context) { super(message); this.name = this.constructor.name; this.registryType = registryType; this.context = context; const ErrorConstructor = Error; if (typeof ErrorConstructor.captureStackTrace === "function") { ErrorConstructor.captureStackTrace(this, this.constructor); } } getDetails() { const details = [this.message]; if (this.registryType) { details.push(`Registry Type: ${this.registryType}`); } if (this.context) { details.push(`Context: ${JSON.stringify(this.context, null, 2)}`); } return details.join("\n"); } }; var RegistryCreationError = class extends RegistryError { constructor(type, reason, context) { super(`Failed to create registry of type '${type}': ${reason}`, type, context); } }; var InvalidFactoryResultError = class extends RegistryError { keyPath; factoryResult; constructor(keyPath, factoryResult, registryType) { const keyPathStr = keyPath.join("."); super( `Factory did not return a valid instance for: ${keyPathStr}. Expected instance with 'coordinate' and 'registry' properties, got: ${typeof factoryResult}`, registryType, { keyPath, factoryResult: typeof factoryResult } ); this.keyPath = keyPath; this.factoryResult = factoryResult; } }; var InvalidInstanceRegistrationError = class extends RegistryError { keyPath; attemptedRegistration; constructor(keyPath, attemptedRegistration, registryType) { const keyPathStr = keyPath.join("."); super( `Attempting to register a non-instance: ${keyPathStr}. Expected instance with 'coordinate' and 'registry' properties, got: ${typeof attemptedRegistration}`, registryType, { keyPath, attemptedRegistration: typeof attemptedRegistration } ); this.keyPath = keyPath; this.attemptedRegistration = attemptedRegistration; } }; // src/errors/RegistryHubError.ts var RegistryHubError = class extends RegistryError { hubType; constructor(message, hubType, context) { const enrichedContext = hubType ? { ...context, hubType } : context; super(message, "", enrichedContext); this.hubType = hubType; } }; var DuplicateRegistryTypeError = class extends RegistryHubError { duplicateType; constructor(type, context) { super( `Registry already registered under type: ${type}. Each registry type must be unique within a registry hub.`, "", { ...context, duplicateType: type } ); this.duplicateType = type; } }; var RegistryTypeNotFoundError = class extends RegistryHubError { requestedType; availableTypes; constructor(requestedType, availableTypes = [], context) { let message = `No registry registered under type: ${requestedType}`; if (availableTypes.length > 0) { message += `. Available types: [${availableTypes.join(", ")}]`; } super(message, "", { ...context, requestedType, availableTypes }); this.requestedType = requestedType; this.availableTypes = availableTypes; } }; var RegistryFactoryError = class extends RegistryHubError { factoryError; attemptedType; constructor(type, factoryError, context) { super( `Registry factory failed to create registry of type '${type}': ${factoryError.message}`, "", { ...context, attemptedType: type, originalError: factoryError.message } ); this.factoryError = factoryError; this.attemptedType = type; } }; var InvalidRegistryFactoryResultError = class extends RegistryHubError { factoryResult; attemptedType; constructor(type, factoryResult, context) { super( `Registry factory returned invalid registry for type '${type}'. Expected registry with 'type', 'get', 'register', and 'createInstance' properties, got: ${typeof factoryResult}`, "", { ...context, attemptedType: type, factoryResult: typeof factoryResult } ); this.factoryResult = factoryResult; this.attemptedType = type; } }; // src/RegistryHub.ts var logger3 = logger_default.get("RegistryHub"); var createRegistryHub = () => { const registries = {}; const createRegistry2 = (type, factory) => { logger3.debug(`Creating new registry with type: ${type}`); if (registries[type]) { throw new DuplicateRegistryTypeError(type); } const registry = factory(type, hub); if (!("registryHub" in registry) || registry.registryHub !== hub) { registry.registryHub = hub; } registries[type] = registry; logger3.debug(`Successfully created and registered new registry with type: ${type}`); return registry; }; const registerRegistry = (registry) => { const type = registry.type; logger3.debug(`Registering registry with type: ${type}`); if (registries[type]) { throw new DuplicateRegistryTypeError(type); } registries[type] = registry; if (!("registryHub" in registry) || registry.registryHub !== hub) { registry.registryHub = hub; } logger3.debug(`Successfully registered registry with type: ${type}`); }; const get = (type, kta, options) => { logger3.debug(`Looking up instance for type: ${type}, kta: ${kta.join(".")}, scopes: ${options?.scopes?.join(",") || "none"}`); const registry = registries[type]; if (!registry) { const availableTypes = Object.keys(registries); throw new RegistryTypeNotFoundError(type, availableTypes); } return registry.get(kta, options); }; const getRegistry = (type) => { return registries[type] || null; }; const getRegisteredTypes = () => { return Object.keys(registries); }; const unregisterRegistry = (type) => { if (registries[type]) { delete registries[type]; logger3.debug(`Unregistered registry under type: ${type}`); return true; } return false; }; const getAllCoordinates = () => { const allCoordinates = []; for (const registryType in registries) { const registry = registries[registryType]; const coordinates = registry.getCoordinates(); coordinates.forEach((coordinate) => { allCoordinates.push({ coordinate, registryType }); }); } logger3.debug(`Retrieved ${allCoordinates.length} total coordinates from ${Object.keys(registries).length} registries`); return allCoordinates; }; const hub = { createRegistry: createRegistry2, registerRegistry, get, getRegistry, getRegisteredTypes, getAllCoordinates, unregisterRegistry }; return hub; }; // src/errors/InstanceError.ts var InstanceError = class extends RegistryError { keyPath; constructor(message, keyPath, registryType, context) { super(message, registryType, { ...context, keyPath }); this.keyPath = keyPath; } }; var InstanceNotFoundError = class extends InstanceError { missingKey; constructor(keyPath, missingKey, registryType, context) { const keyPathStr = keyPath.join("."); let message = `Instance not found for key path: ${keyPathStr}`; if (missingKey) { message += `, Missing key: ${missingKey}`; } super(message, keyPath, registryType, { ...context, missingKey }); this.missingKey = missingKey; } }; var NoInstancesRegisteredError = class extends InstanceError { constructor(keyPath, registryType, context) { const keyPathStr = keyPath.join("."); super( `No instances registered for key path: ${keyPathStr}. The key path exists in the registry tree but contains no instances.`, keyPath, registryType, context ); } }; var NoInstancesAvailableError = class extends InstanceError { constructor(keyPath, registryType, context) { const keyPathStr = keyPath.join("."); super( `No instances available for key path: ${keyPathStr}. This typically indicates an internal registry state issue.`, keyPath, registryType, context ); } }; var ScopeNotFoundError = class extends InstanceError { requestedScopes; availableScopes; constructor(keyPath, requestedScopes, availableScopes = [], registryType) { const keyPathStr = keyPath.join("."); const scopesStr = requestedScopes.join(", "); const availableScopesStr = availableScopes.map((scopes) => `[${scopes.join(", ")}]`).join(", "); let message = `No instance found matching scopes: ${scopesStr} for key path: ${keyPathStr}`; if (availableScopes.length > 0) { message += `. Available scopes: ${availableScopesStr}`; } super(message, keyPath, registryType, { requestedScopes, availableScopes }); this.requestedScopes = requestedScopes; this.availableScopes = availableScopes; } }; var NoChildrenAvailableError = class extends InstanceError { parentKey; constructor(keyPath, parentKey, registryType, context) { const keyPathStr = keyPath.join("."); super( `Instance not found for key path: ${keyPathStr}, No children for: ${parentKey}. The path cannot be traversed further as '${parentKey}' has no child nodes.`, keyPath, registryType, { ...context, parentKey } ); this.parentKey = parentKey; } }; // src/errors/CoordinateError.ts var CoordinateError = class extends RegistryError { kta; scopes; constructor(message, kta, scopes, context) { super(message, "", { ...context, kta, scopes }); this.kta = kta; this.scopes = scopes; } }; var InvalidCoordinateError = class extends CoordinateError { constructor(kta, scopes, reason, context) { super( `Invalid coordinate parameters: ${reason}. KTA: ${JSON.stringify(kta)}, Scopes: [${scopes.join(", ")}]`, kta, scopes, { ...context, reason } ); } }; var InvalidKTAError = class extends CoordinateError { constructor(kta, reason, context) { super( `Invalid KTA (Key Type Array): ${reason}. Expected string or array of strings, got: ${JSON.stringify(kta)}`, kta, [], { ...context, reason } ); } }; var InvalidScopesError = class extends CoordinateError { invalidScopes; constructor(scopes, invalidScopes, reason, context) { super( `Invalid scopes: ${reason}. Invalid scope values: ${JSON.stringify(invalidScopes)}`, null, scopes.filter((s) => typeof s === "string"), { ...context, reason, invalidScopes } ); this.invalidScopes = invalidScopes; } }; export { CoordinateError, DuplicateRegistryTypeError, InstanceError, InstanceNotFoundError, InvalidCoordinateError, InvalidFactoryResultError, InvalidInstanceRegistrationError, InvalidKTAError, InvalidRegistryFactoryResultError, InvalidScopesError, NoChildrenAvailableError, NoInstancesAvailableError, NoInstancesRegisteredError, RegistryCreationError, RegistryError, RegistryFactoryError, RegistryHubError, RegistryStats, RegistryTypeNotFoundError, ScopeNotFoundError, createInstance, createRegistry, createRegistryHub, isInstance };