muddle/core/metadata.ts
2026-10-23 08:53:07 -04:00

179 lines
5.3 KiB
TypeScript

export type MetadataKey = string | symbol;
type MetadataMap = Map<MetadataKey, unknown[]>;
type PropertyMetadataMap = Map<MetadataKey, MetadataMap>;
const metadataStore = new WeakMap<object, MetadataMap>();
const propertyMetadataStore = new WeakMap<object, PropertyMetadataMap>();
function mapFor(target: object): MetadataMap {
let map = metadataStore.get(target);
if (!map) {
map = new Map();
metadataStore.set(target, map);
}
return map;
}
function propertyMapFor(target: object, propertyKey: MetadataKey): MetadataMap {
let properties = propertyMetadataStore.get(target);
if (!properties) {
properties = new Map();
propertyMetadataStore.set(target, properties);
}
let map = properties.get(propertyKey);
if (!map) {
map = new Map();
properties.set(propertyKey, map);
}
return map;
}
/** Stores `metadataValue` for `metadataKey` on `target` (and `propertyKey`). */
export function defineMetadata(
metadataKey: MetadataKey,
metadataValue: unknown,
target: object,
propertyKey?: MetadataKey,
): void {
const map = mapFor(target);
const existing = map.get(metadataKey) ?? [];
existing.push(metadataValue);
map.set(metadataKey, existing);
if (propertyKey !== undefined) {
const propertyMap = propertyMapFor(target, propertyKey);
const propertyExisting = propertyMap.get(metadataKey) ?? [];
propertyExisting.push(metadataValue);
propertyMap.set(metadataKey, propertyExisting);
}
}
/** Reads metadata declared directly on `target`. */
export function getOwnMetadata(
metadataKey: MetadataKey,
target: object,
propertyKey?: MetadataKey,
): unknown | undefined {
if (propertyKey !== undefined) {
const properties = propertyMetadataStore.get(target);
return properties?.get(propertyKey)?.get(metadataKey)?.[0];
}
return metadataStore.get(target)?.get(metadataKey)?.[0];
}
/** Reads metadata declared on `target` or inherited from its prototype chain. */
export function getMetadata(
metadataKey: MetadataKey,
target: object,
propertyKey?: MetadataKey,
): unknown | undefined {
let current: object | null = target;
while (current) {
const value = getOwnMetadata(metadataKey, current, propertyKey);
if (value !== undefined) return value;
current = Object.getPrototypeOf(current);
}
return undefined;
}
/** `true` when metadata is declared on `target` or an ancestor. */
export function hasMetadata(
metadataKey: MetadataKey,
target: object,
propertyKey?: MetadataKey,
): boolean {
return getMetadata(metadataKey, target, propertyKey) !== undefined;
}
/** Removes all metadata registered for `target`. */
export function deleteMetadata(target: object): void {
metadataStore.delete(target);
}
/** Reads the most recent metadata entry declared for `target`. */
export function getLatestMetadata(
metadataKey: MetadataKey,
target: object,
propertyKey?: MetadataKey,
): unknown | undefined {
if (propertyKey !== undefined) {
const properties = propertyMetadataStore.get(target);
const entries = properties?.get(propertyKey)?.get(metadataKey);
return entries?.[entries.length - 1];
}
const entries = metadataStore.get(target)?.get(metadataKey);
return entries?.[entries.length - 1];
}
/** Owner of a decorated member. */
export function ownerOf(target: object): object {
if (typeof target === "function") return target as object;
return (target as { constructor: object }).constructor;
}
/** Stores metadata of a method or a parameter on the owning class. */
export function defineMemberMetadata(
metadataKey: MetadataKey,
metadataValue: unknown,
target: object,
propertyKey?: MetadataKey,
): void {
defineMetadata(metadataKey, metadataValue, ownerOf(target), propertyKey);
}
/** Reads every metadata entry stored for a method. */
export function getMemberMetadata(
metadataKey: MetadataKey,
target: object,
propertyKey: MetadataKey,
): unknown[] {
const properties = propertyMetadataStore.get(target);
const map = properties?.get(propertyKey);
if (!map) return [];
return map.get(metadataKey) ?? [];
}
/** Reads every metadata entry of a class, together with its property key. */
export function getAllMemberMetadata(
metadataKey: MetadataKey,
target: object,
): { propertyKey: MetadataKey; value: unknown }[] {
const properties = propertyMetadataStore.get(target);
if (!properties) return [];
const entries: { propertyKey: MetadataKey; value: unknown }[] = [];
for (const [propertyKey, map] of properties) {
for (const value of map.get(metadataKey) ?? []) {
entries.push({ propertyKey, value });
}
}
return entries;
}
/** Installs the proposal API on `Reflect` when the runtime does not provide it. */
export function installMetadataPolyfill(): void {
const reflect = Reflect as unknown as Record<string, unknown>;
if (typeof reflect["metadata"] !== "function") {
reflect["metadata"] = (
metadataKey: MetadataKey,
metadataValue: unknown,
) => {
return (target: object, propertyKey?: MetadataKey): void => {
defineMetadata(metadataKey, metadataValue, target, propertyKey);
};
};
}
if (typeof reflect["defineMetadata"] !== "function") {
reflect["defineMetadata"] = defineMetadata;
}
if (typeof reflect["getOwnMetadata"] !== "function") {
reflect["getOwnMetadata"] = getOwnMetadata;
}
if (typeof reflect["getMetadata"] !== "function") {
reflect["getMetadata"] = getMetadata;
}
if (typeof reflect["hasMetadata"] !== "function") {
reflect["hasMetadata"] = hasMetadata;
}
}
installMetadataPolyfill();