@fjell/registry
Version:
Dependency injection and service location system for the Fjell ecosystem
698 lines (688 loc) • 22.7 kB
JavaScript
// 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
};