/** * Decorator utilities for Blok editor */ type CacheableAccessor = { get?: () => Value; set?: (value: Value) => void; init?: (value: Value) => Value; }; type Stage3DecoratorContext = { kind: 'method' | 'getter' | 'setter' | 'accessor'; name: string | symbol; static?: boolean; private?: boolean; access?: { get?: () => unknown; set?: (value: unknown) => void; }; }; type CacheableDecorator = { ( target: Record, propertyKey: string | symbol, descriptor?: TypedPropertyDescriptor ): TypedPropertyDescriptor | void; ( value: ((...args: Arguments) => Value) | CacheableAccessor, context: Stage3DecoratorContext ): | ((...args: Arguments) => Value) | CacheableAccessor; }; const ensureCacheValue = ( holder: Record, cacheKey: string | symbol, compute: () => Value ): Value => { if (!Reflect.has(holder, cacheKey)) { Object.defineProperty(holder, cacheKey, { configurable: true, writable: true, value: compute(), }); } return Reflect.get(holder, cacheKey) as Value; }; const clearCacheValue = (holder: Record, cacheKey: string | symbol): void => { if (Reflect.has(holder, cacheKey)) { Reflect.deleteProperty(holder, cacheKey); } }; const isStage3DecoratorContext = (context: unknown): context is Stage3DecoratorContext => { if (typeof context !== 'object' || context === null) { return false; } return 'kind' in context && 'name' in context; }; const buildLegacyCacheableDescriptor = ( target: Record, propertyKey: string | symbol, descriptor?: TypedPropertyDescriptor ): TypedPropertyDescriptor => { const baseDescriptor: TypedPropertyDescriptor = descriptor ?? Object.getOwnPropertyDescriptor(target, propertyKey) ?? (typeof target === 'function' ? Object.getOwnPropertyDescriptor((target as unknown as { prototype?: Record }).prototype ?? {}, propertyKey) : undefined) ?? { configurable: true, enumerable: false, writable: true, value: Reflect.get(target, propertyKey) as unknown, }; const descriptorRef = { ...baseDescriptor } as TypedPropertyDescriptor; const cacheKey: string | symbol = typeof propertyKey === 'symbol' ? propertyKey : `#${propertyKey}Cache`; const hasMethodValue = descriptorRef.value !== undefined && typeof descriptorRef.value === 'function'; const shouldWrapGetter = !hasMethodValue && descriptorRef.get !== undefined; const shouldWrapSetter = shouldWrapGetter && descriptorRef.set !== undefined; if (hasMethodValue) { const originalMethod = descriptorRef.value as (...methodArgs: unknown[]) => unknown; descriptorRef.value = function (this: unknown, ...methodArgs: unknown[]): unknown { return ensureCacheValue(this as Record, cacheKey, () => originalMethod.apply(this, methodArgs)); }; } if (shouldWrapGetter && descriptorRef.get !== undefined) { const originalGetter = descriptorRef.get; descriptorRef.get = function (this: unknown): unknown { return ensureCacheValue(this as Record, cacheKey, () => originalGetter.call(this)); }; } if (shouldWrapSetter && descriptorRef.set !== undefined) { const originalSetter = descriptorRef.set; descriptorRef.set = function (this: unknown, newValue: unknown): void { clearCacheValue(this as Record, cacheKey); (originalSetter).call(this, newValue); }; } if (!descriptor) { return descriptorRef; } Object.keys(descriptor).forEach(propertyName => { if (!(propertyName in descriptorRef)) { Reflect.deleteProperty(descriptor, propertyName); } }); Object.assign(descriptor, descriptorRef); return descriptor; }; const applyStage3CacheableDecorator = ( value: ((...methodArgs: unknown[]) => unknown) | CacheableAccessor, context: Stage3DecoratorContext ): unknown => { const cacheKey = Symbol( typeof context.name === 'symbol' ? `cache:${context.name.description ?? 'symbol'}` : `cache:${context.name}` ); if (context.kind === 'method' && typeof value === 'function') { const originalMethod = value; return function (this: Record, ...methodArgs: unknown[]): unknown { return ensureCacheValue(this, cacheKey, () => originalMethod.apply(this, methodArgs)); }; } if (context.kind === 'getter' && typeof value === 'function') { const originalGetter = value; return function (this: Record): unknown { return ensureCacheValue(this, cacheKey, () => originalGetter.call(this)); }; } if (context.kind === 'accessor' && typeof value === 'object' && value !== null) { const accessor = value; const fallbackGetter = accessor.get ?? context.access?.get; const fallbackSetter = accessor.set ?? context.access?.set; return { get(this: Record): unknown { return fallbackGetter ? ensureCacheValue(this, cacheKey, () => fallbackGetter.call(this)) : undefined; }, set(this: Record, newValue: unknown): void { clearCacheValue(this, cacheKey); fallbackSetter?.call(this, newValue); }, init(initialValue: unknown): unknown { return accessor.init ? accessor.init(initialValue) : initialValue; }, } satisfies CacheableAccessor; } return value; }; /** * Decorator which provides ability to cache method or accessor result. * Supports both legacy and TC39 stage 3 decorator semantics. * @param args - decorator arguments (legacy: target, propertyKey, descriptor. Stage 3: value, context) */ const cacheableImpl = (...args: unknown[]): unknown => { if (args.length === 2 && isStage3DecoratorContext(args[1])) { const [value, context] = args as [ ((...methodArgs: unknown[]) => unknown) | CacheableAccessor, Stage3DecoratorContext ]; return applyStage3CacheableDecorator(value, context); } const [target, propertyKey, descriptor] = args as [ Record, string | symbol, TypedPropertyDescriptor | undefined ]; return buildLegacyCacheableDescriptor(target, propertyKey, descriptor); }; export const cacheable = cacheableImpl as CacheableDecorator;