impair
Version:
A framework for building React applications with OOP principles and a layered architecture.
667 lines (637 loc) • 21.9 kB
JavaScript
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