UNPKG

impair

Version:

A framework for building React applications with OOP principles and a layered architecture.

667 lines (637 loc) 21.9 kB
import { effectScope, computed, shallowReactive, shallowRef, ref, effect as effect$1, stop, pauseTracking, enableTracking, shallowReadonly } from '@vue/reactivity'; export { enableTracking, pauseTracking, toRaw, toReadonly } from '@vue/reactivity'; import { createContext, useMemo, useEffect, useContext, memo, useRef, createElement, useState, useCallback } from 'react'; import { container, Lifecycle, scoped, injectable as injectable$1 } from 'tsyringe'; export { delay, inject } from 'tsyringe'; const isMounted = Symbol('isServiceMounted'); const isLifecycleHandled = Symbol('isLifecycleHandled'); const isInitialized$1 = Symbol('isInitialized'); const stateMetadataKey = Symbol('state'); const triggerMetadataKey = Symbol('trigger'); const derivedMetadataKey = Symbol('derived'); const injectableMetadataKey = Symbol('injectable'); const provideMetadataKey = Symbol('provide'); const onMountMetadataKey = Symbol('onMount'); const onUnmountMetadataKey = Symbol('onUnmount'); const onInitMetadataKey = Symbol('onInit'); const onDestroyMetadataKey = Symbol('onDestroy'); const isContainerDisposed = Symbol('isContainerDisposed'); function disposeContainer(container) { if (!container[isContainerDisposed]) { container[isContainerDisposed] = true; container.dispose(); } } const Context = createContext(container); function debounceMicrotask(fn) { let called = false; return () => { if (!called) { called = true; queueMicrotask(() => { called = false; fn(); }); } }; } function onDispose(target, propertyKey) { if (propertyKey !== 'dispose') { if (!target['dispose']) { Object.defineProperty(target, 'dispose', { value: function dispose() { }, writable: true, configurable: true, }); } const onDisposes = Reflect.getMetadata(onDestroyMetadataKey, target) ?? []; onDisposes.push(propertyKey); Reflect.metadata(onDestroyMetadataKey, onDisposes)(target); } } function initOnDispose(instance, disposers) { const onDisposeProperties = Reflect.getMetadata(onDestroyMetadataKey, instance) ?? []; onDisposeProperties.forEach((propName) => { const disposeFn = instance[propName]; disposers.push(() => { disposeFn.call(instance); }); }); } function onInit(target, propertyKey) { const onInits = Reflect.getMetadata(onInitMetadataKey, target) ?? []; onInits.push(propertyKey); Reflect.metadata(onInitMetadataKey, onInits)(target); } function initOnInit(instance) { const onInitProperties = Reflect.getMetadata(onInitMetadataKey, instance); if (onInitProperties) { onInitProperties.forEach((propName) => { const initFn = instance[propName]; initFn.call(instance); }); } } function derived(target, propertyKey, descriptor) { const propNames = Reflect.getMetadata(derivedMetadataKey, target) ?? []; propNames.push({ propertyKey, descriptor, }); return Reflect.metadata(derivedMetadataKey, propNames)(target); } function initDerived({ disposers, instance }) { const cachedProperties = Reflect.getMetadata(derivedMetadataKey, instance); if (cachedProperties) { cachedProperties.forEach(({ propertyKey, descriptor }) => { const getter = descriptor.get; let computedValue; const scope = effectScope(); scope.run(() => { computedValue = computed(() => { return getter.call(instance); }); }); disposers.push(() => { scope.stop(); }); Object.defineProperty(instance, propertyKey, { enumerable: true, configurable: true, get() { return computedValue.value; }, }); }); } } function registerStateMetadata(target, metadata) { const statePropMetadata = Reflect.getMetadata(stateMetadataKey, target) ?? []; statePropMetadata.push(metadata); return Reflect.metadata(stateMetadataKey, statePropMetadata)(target); } function state(target, propertyKey) { return registerStateMetadata(target, { propertyKey, type: 'deep', }); } function shallowState(target, propertyKey) { return registerStateMetadata(target, { propertyKey, type: 'shallow', }); } function atomState(target, propertyKey) { return registerStateMetadata(target, { propertyKey, type: 'atom', }); } state.shallow = shallowState; state.atom = atomState; function createReactiveState(initialValue, type) { if (type === 'atom') { return shallowRef(initialValue); } if (type === 'deep') { return ref(initialValue); } if (type === 'shallow') { if (initialValue != null && typeof initialValue === 'object') { return shallowRef(shallowReactive(initialValue)); } else { return shallowRef(initialValue); } } return ref(initialValue); } function initState({ instance }) { const stateValueMap = new Map(); const stateProperties = Reflect.getMetadata(stateMetadataKey, instance); if (stateProperties) { stateProperties.forEach(({ propertyKey, type }) => { const initialValue = instance[propertyKey]; const reactiveState = createReactiveState(initialValue, type); stateValueMap.set(propertyKey, reactiveState); Object.defineProperty(instance, propertyKey, { get() { return stateValueMap.get(propertyKey)?.value; }, set(newValue) { const refValue = stateValueMap.get(propertyKey); if (refValue) { refValue.value = type === 'shallow' && newValue != null && typeof newValue === 'object' ? shallowReactive(newValue) : newValue; } else { console.error(`No ref value found for ${propertyKey}`); } }, }); }); } } function effect(fn, options) { let cleanup; return effect$1(() => { if (cleanup) { cleanup(); } const effectValue = fn(); if (typeof effectValue === 'function') { cleanup = effectValue; } else { cleanup = undefined; } }, options); } function asyncEffect(fn) { const callRunner = debounceMicrotask(() => { runner(); }); const runner = effect(fn, { scheduler() { callRunner(); }, }); return runner; } function addTriggerMetadata(target, propertyKey, flush = 'sync') { const triggerInfoArr = Reflect.getMetadata(triggerMetadataKey, target) ?? []; triggerInfoArr.push({ propertyKey, flush, }); return Reflect.metadata(triggerMetadataKey, triggerInfoArr)(target); } function trigger(target, propertyKey) { return addTriggerMetadata(target, propertyKey); } trigger.async = function (target, propertyKey) { return addTriggerMetadata(target, propertyKey, 'async'); }; function initTrigger({ instance, disposers }) { const triggerProperties = Reflect.getMetadata(triggerMetadataKey, instance); if (triggerProperties) { triggerProperties.forEach(({ flush, propertyKey }) => { const effectFn = instance[propertyKey]; const effectRunner = flush === 'sync' ? effect : asyncEffect; const runner = effectRunner(() => { return effectFn.call(instance); }); disposers.push(() => { stop(runner); }); Object.defineProperty(instance, propertyKey, { enumerable: true, configurable: true, writable: true, value: () => { runner(); }, }); }); } } function untrack(fn) { pauseTracking(); const result = fn(); enableTracking(); return result; } function getAllMethods(obj) { const properties = new Set(); let currentObj = obj; while (currentObj !== null && currentObj !== Object.prototype) { Object.getOwnPropertyNames(currentObj).forEach((prop) => { if (prop !== 'constructor' && Object.getOwnPropertyDescriptor(currentObj, prop)?.value instanceof Function) { properties.add(prop); } }); currentObj = Object.getPrototypeOf(currentObj); } return [...properties]; } function bindMethods(instance) { getAllMethods(instance).forEach((key) => { instance[key] = instance[key].bind(instance); }); return instance; } function patchClassInstanceMethod(instance, methodName, callback) { const originalMethod = instance[methodName]; if (originalMethod) { Object.defineProperty(instance, methodName, { value: function () { originalMethod.call(instance); callback(); }, configurable: true, writable: true, }); } else { Object.defineProperty(instance, methodName, { value: function () { callback(); }, configurable: true, writable: true, }); } return instance; } function initInstance(instance) { if (!isInitialized(instance)) { try { const disposers = []; setInitialized(instance); patchClassInstanceMethod(instance, 'dispose', function dispose() { disposers.forEach((dispose) => { dispose(); }); }); const params = { instance, disposers, }; initState(params); initDerived(params); initTrigger(params); bindMethods(instance); initOnInit(instance); initOnDispose(instance, disposers); } catch (error) { console.error('Impair Error initializing instance', instance, error); setInitialized(instance, false); } } return instance; } function setInitialized(instance, value = true) { instance[isInitialized$1] = value; } function isInitialized(instance) { return instance[isInitialized$1] ?? false; } function isInjectionToken(token) { return typeof token === 'function' || typeof token === 'symbol' || typeof token === 'string'; } function isInjectableClass(instance) { const constructor = Object.getPrototypeOf(instance).constructor; return typeof constructor === 'function' && Reflect.getMetadata(injectableMetadataKey, constructor); } function createChildContainer(parentContainer) { const container = parentContainer.createChildContainer(); const resolve = container.resolve; const tokens = new Set(); container.resolve = function (...args) { const token = args[0]; if (!tokens.has(token) && isInjectionToken(token)) { tokens.add(token); container.afterResolution(token, (_, instance) => { if (isInjectableClass(instance)) { return initInstance(instance); } }, { frequency: 'Always', }); } return resolve.call(container, ...args); }; return container; } class Container { container; constructor(container) { this.container = container; this.register = this.container.register.bind(this.container); } register; resolve(token) { return initInstance(this.container.resolve(token)); } } const providerPropsSymbol = Symbol('ProviderProps'); const Props = providerPropsSymbol; function useReactiveProps(props) { const { reactiveProps, next } = useMemo(() => { const reactiveProps = shallowReactive({ ...props }); return { next(nextProps) { Object.keys(nextProps).forEach((key) => { reactiveProps[key] = nextProps[key]; }); }, reactiveProps: shallowReadonly(reactiveProps), }; }, []); useEffect(() => { if (props) { next(props); } }, [next, props]); return reactiveProps; } function toLifecycle(lifecycle) { switch (lifecycle) { case 'singleton': return Lifecycle.Singleton; case 'transient': return Lifecycle.Transient; case 'container': return Lifecycle.ContainerScoped; case 'resolution': return Lifecycle.ResolutionScoped; default: throw new Error('Invalid lifecycle'); } } function getRegistrationOptions(registration) { /** * If the registration is a function, * it means that it is a class to be registered as singleton */ if (typeof registration === 'function') { const serviceClass = registration; return { token: serviceClass, provider: { useClass: serviceClass, }, lifecycle: 'singleton', }; } /** * The registration is [token, class, lifecycle] or [class, lifecycle], */ if (Array.isArray(registration)) { if (typeof registration[1] === 'string') { const [serviceClass, lifecycle] = registration; return { token: serviceClass, provider: { useClass: serviceClass, }, lifecycle, }; } const [serviceToken, providedClass, lifecycle = 'singleton'] = registration; return { token: serviceToken, provider: { useClass: providedClass, }, lifecycle, }; } /** * If the registration is an object, * it means that it is a custom registration */ if (typeof registration === 'object') { if (!registration.lifecycle) { return { ...registration, lifecycle: 'singleton', }; } return registration; } throw new Error('Invalid service provider registration'); } function registerServices(container, services) { const resolvedServices = new Set(); services.forEach((serviceInfo) => { const { provider, token, lifecycle } = getRegistrationOptions(serviceInfo); container.register(token, provider, { lifecycle: toLifecycle(lifecycle), }); container.afterResolution(token, (_, result) => { if (!result[isLifecycleHandled]) { result[isLifecycleHandled] = true; resolvedServices.add(result); } }, { frequency: 'Once' }); }); return resolvedServices; } function onMount(target, propertyKey) { const onMounts = Reflect.getMetadata(onMountMetadataKey, target) ?? []; onMounts.push(propertyKey); Reflect.metadata(onMountMetadataKey, onMounts)(target); } function getOnMountMethods(instance) { const onMountProperties = Reflect.getMetadata(onMountMetadataKey, instance) ?? []; return onMountProperties.map((propName) => { const mountFn = instance[propName]; return mountFn.bind(instance); }); } function onUnmount(target, propertyKey) { const onUnmounts = Reflect.getMetadata(onUnmountMetadataKey, target) ?? []; onUnmounts.push(propertyKey); Reflect.metadata(onUnmountMetadataKey, onUnmounts)(target); } function getOnUnmountMethods(instance) { const onUnmountProperties = Reflect.getMetadata(onUnmountMetadataKey, instance) ?? []; return onUnmountProperties.map((propName) => { const unmountFn = instance[propName]; return unmountFn.bind(instance); }); } function useHandleLifecycle(container, resolvedServices) { useEffect(() => { const disposers = [...resolvedServices].map((service) => { if (!service[isMounted]) { service[isMounted] = true; const onMounts = getOnMountMethods(service); const onMountDisposers = onMounts.map((onMount) => onMount()).filter((p) => typeof p === 'function'); return () => { if (service[isMounted]) { service[isMounted] = false; const onUnmounts = getOnUnmountMethods(service); onUnmounts.concat(onMountDisposers).forEach((dispose) => dispose()); } }; } }); return () => { disposers.forEach((dispose) => { dispose?.(); }); disposeContainer(container); }; }, [container]); return resolvedServices; } function useRegisteredContainer(props, services, existingContainer) { const parentContainer = useContext(Context); const mappedProps = useReactiveProps(props ?? {}); const { container, resolvedServices } = useMemo(() => { const container = existingContainer ?? createChildContainer(parentContainer); if (!container.isRegistered(Container)) { container.register(Container, { useValue: new Container(container), }); } if (!container.isRegistered(Props)) { container.register(Props, { useValue: mappedProps, }); } const resolvedServices = registerServices(container, services); return { container, resolvedServices, }; }, [existingContainer, parentContainer]); useHandleLifecycle(container, resolvedServices); return container; } let currentComponentContainerRef; function setCurrentComponentContainerRef(containerRef) { currentComponentContainerRef = containerRef; } function useViewModel(viewModel, props) { const container = useContext(Context); if (currentComponentContainerRef && !currentComponentContainerRef.current) { currentComponentContainerRef.current = createChildContainer(container); } const componentContainer = currentComponentContainerRef.current; const viewModelProviders = useMemo(() => { const provided = Reflect.getMetadata(provideMetadataKey, viewModel) ?? []; return [...provided, viewModel]; }, [viewModel]); useRegisteredContainer(props, viewModelProviders, componentContainer); const instance = componentContainer.resolve(viewModel); return instance; } function useForceUpdate() { const [_, setVal] = useState({}); return useCallback(() => { setVal({}); }, []); } function component(component) { return memo((props) => { const forceUpdate = useForceUpdate(); const renderResult = useRef(null); const runner = useRef(undefined); const propsRef = useRef(props); const isDirty = useRef(false); const componentContainer = useRef(undefined); if (!runner.current) { const render = debounceMicrotask(() => { if (isDirty.current) { forceUpdate(); } }); runner.current = effect$1(() => { setCurrentComponentContainerRef(componentContainer); renderResult.current = component(propsRef.current); }, { scheduler() { isDirty.current = true; render(); }, }); } else { runner.current?.(); } useEffect(() => { forceUpdate(); return () => { if (runner.current) { stop(runner.current); } runner.current = undefined; if (componentContainer.current) { disposeContainer(componentContainer.current); componentContainer.current = undefined; } }; }, []); if (componentContainer.current) { return createElement(Context.Provider, { value: componentContainer.current }, renderResult.current); } return renderResult.current; }); } component.fromViewModel = (viewModel) => { const comp = component((props) => useViewModel(viewModel, props).render()); comp.displayName = viewModel.name.replace('ViewModel', ''); return comp; }; function useService(service) { return useContext(Context).resolve(service); } function injectable(scope) { return function (target) { if (scope != null) { scoped(scope === 'container-scoped' ? Lifecycle.ContainerScoped : Lifecycle.ResolutionScoped)(target); } else { injectable$1()(target); } Reflect.metadata(injectableMetadataKey, true)(target); }; } function provide(registrations) { return function (target) { Reflect.metadata(provideMetadataKey, registrations)(target); }; } function ServiceProvider({ provide, children, props = {}, }) { const container = useRegisteredContainer(props, provide); return createElement(Context.Provider, { value: container }, children); } export { Props, ServiceProvider, component, derived, injectable, onDispose, onInit, onMount, onUnmount, provide, state, trigger, untrack, useService, useViewModel }; //# sourceMappingURL=index.js.map