@ryo-98/react-api-bridge
Version:
A registry for component imperative api
407 lines (404 loc) • 12.9 kB
JavaScript
// ../src/bridge.tsx
import React, {
createContext,
createRef,
forwardRef,
useCallback,
useContext,
useEffect,
useImperativeHandle,
useMemo,
useRef
} from "react";
// ../src/utils.ts
var removeArrayElement = (entity, element) => {
if (Array.isArray(entity)) {
const deleteIndex = entity.findIndex((r) => r === element);
if (deleteIndex > -1) {
entity.splice(deleteIndex, 1);
}
}
};
function tryInvoke(mayFn) {
if (typeof mayFn === "function") {
mayFn();
return true;
}
return false;
}
var prefixes = [];
var lastOccupiedNumber = 0;
var genHookId = () => {
if (lastOccupiedNumber < Number.MAX_SAFE_INTEGER) {
lastOccupiedNumber++;
} else {
prefixes.push(String.fromCharCode(prefixes.length));
lastOccupiedNumber = 0;
}
return [...prefixes, lastOccupiedNumber].join("");
};
function appendToMappedValue(map, key, element) {
let arr = map.get(key);
if (typeof arr === "undefined") {
arr = [element];
map.set(key, arr);
} else {
arr.push(element);
}
return () => {
removeArrayElement(arr, element);
if (arr?.length === 0) {
map.delete(key);
}
};
}
// ../src/bridge.tsx
var createBridge = (...args) => {
const payload = args[0];
const output = genOutput(payload);
const currying = (options) => {
return genOutput(payload, options);
};
return Object.assign(currying, output);
};
var bridge_default = createBridge;
function genOutput(payload, bridgeOptions) {
const defaultContextValue = {
bridge: {},
parent: void 0,
payload
};
const BridgeContext = createContext(defaultContextValue);
const cacheInitCbMap = /* @__PURE__ */ new WeakMap();
const pendingResolverMap = /* @__PURE__ */ new Map();
const _getPendingContextMap = (_apiNList) => {
return pendingResolverMap.get(_apiNList);
};
const _setPendingContextMap = (_apiNList, map) => {
return pendingResolverMap.set(_apiNList, map);
};
function getIsMulti(name) {
return bridgeOptions?.[name]?.isMulti;
}
const _getApiDesc = (name, source) => {
let isInitial = false;
if (!source[name]) {
isInitial = true;
const isMulti = getIsMulti(name);
const apiNList = isMulti ? [] : createRef();
source[name] = {
options: bridgeOptions?.[name],
apiNList
};
}
return { apiNList: source[name].apiNList, isInitial };
};
const Boundary = forwardRef((props, ref) => {
const _ownBridge = useMemo(() => ({}), []);
const bridge = props.contextValue?.bridge || _ownBridge;
const paramParent = props.contextValue?.parent;
const upperParent = useContext(BridgeContext);
const parent = paramParent || upperParent;
const _payload = props.contextValue?.payload ?? props.payload;
useImperativeHandle(ref, () => {
return {
getAPI: (name) => {
return _getApiDesc(name, bridge).apiNList;
},
parent,
payload: _payload
};
}, [_payload]);
const contextValue = useMemo(() => {
const final = {
...props.contextValue || {
bridge,
parent
},
payload: _payload
};
return final;
}, [props.contextValue, _payload]);
return /* @__PURE__ */ React.createElement(BridgeContext.Provider, { value: contextValue }, props.children);
});
Boundary.displayName = "Boundary";
const useFinalContextValue = (options) => {
const { contextValue: _outerContextValue } = options || {};
const ownContextValue = useContext(BridgeContext);
return _outerContextValue || ownContextValue;
};
const initializedOnInitMap = /* @__PURE__ */ new Map();
function mountHookInitEffect(name, onInit, apiNList, hookId) {
if (!onInit) return;
const isMulti = getIsMulti(name) ?? false;
const involvedApiList = [];
let deferFn;
if (apiNList && !isMulti) {
const _assertedApi = apiNList;
if (!_assertedApi.current) return;
const _assertedOnInit = onInit;
deferFn = () => {
return _assertedOnInit(_assertedApi);
};
involvedApiList.push(_assertedApi);
} else if (apiNList && isMulti) {
const _assertedApiList = apiNList;
if (!_assertedApiList.length) return;
const _assertedOnInit = onInit;
deferFn = () => {
return _assertedOnInit(void 0, _assertedApiList);
};
involvedApiList.push(..._assertedApiList);
} else {
throw new Error("This might the internal Error of react-api-bridge");
}
let clearFns = [];
if (involvedApiList.some((apiRef) => !initializedOnInitMap.get(apiRef)?.includes(hookId))) {
clearFns.push(
deferFn?.(),
...involvedApiList.map((apiRef) => {
return appendToMappedValue(initializedOnInitMap, apiRef, hookId);
})
);
}
return () => {
clearFns.forEach(tryInvoke);
};
}
function useInitEffect(onInit, name, apiNList, contextValue) {
const hookId = useHookId();
useEffect(() => {
if (!onInit) return;
const removeCache = appendToMappedValue(cacheInitCbMap, apiNList, { onInit, hookId });
const clearInitEffect = mountHookInitEffect(name, onInit, apiNList, hookId);
return () => {
removeCache();
clearInitEffect?.();
};
}, []);
useEffect(() => {
if (!onInit) return;
const cacheCbs = cacheInitCbMap.get(apiNList);
cacheCbs?.forEach((couple) => {
if (couple.hookId === hookId) {
couple.onInit = onInit;
}
});
}, [onInit]);
useEffect(() => {
return () => {
initializedOnInitMap.forEach((arr, key) => {
removeArrayElement(arr, hookId);
});
};
}, []);
}
function _getUpperContextValue(start, shouldForwardYield) {
let parent = start.parent;
while (true) {
if (!parent) break;
if (!shouldForwardYield) break;
const apiNames = Object.keys(parent.bridge);
const allAPI = apiNames.reduce((all, apiName) => {
const bridge = parent.bridge[apiName];
all[apiName] = bridge.apiNList;
return all;
}, {});
if (!!shouldForwardYield({
payload: parent.payload,
parent: parent.parent,
allAPI
})) break;
parent = parent.parent;
}
return parent;
}
function _getUpperApiDesc(contextValue, _name, options) {
const parent = _getUpperContextValue(options?.contextValue || contextValue, options?.shouldForwardYield);
if (!parent) return;
return _getApiDesc(_name, parent.bridge);
}
function _getResolverOrCreateWhenMissing(apiNList, contextValue, initial) {
let contextValueMap = _getPendingContextMap(apiNList);
if (!contextValueMap) {
contextValueMap = /* @__PURE__ */ new Map();
_setPendingContextMap(apiNList, contextValueMap);
}
let resolvers = contextValueMap.get(contextValue);
if (!resolvers) {
resolvers = [];
contextValueMap.set(contextValue, resolvers);
}
let resolver = resolvers.find((r) => r.initial);
if (!resolver || !initial) {
let outerResolve;
const newPromise = new Promise((resolve) => {
outerResolve = resolve;
});
resolver = {
initial,
promise: newPromise,
resolve: outerResolve
};
resolvers.push(resolver);
}
return {
resolver,
contextValueMap,
resolvers
};
}
return {
Boundary,
initCbMap: cacheInitCbMap,
initializedCallbacksMap: initializedOnInitMap,
getAPI(name, contextValue = defaultContextValue) {
return _getApiDesc(name, contextValue.bridge).apiNList;
},
getAPIAsync(name, options) {
const { contextValue, initial } = {
contextValue: defaultContextValue,
initial: true,
...options
};
const { apiNList } = _getApiDesc(name, contextValue.bridge);
const { resolver } = _getResolverOrCreateWhenMissing(apiNList, contextValue, initial);
return resolver.promise;
},
useAPI: (name, hookOptions) => {
const { onInit } = hookOptions || {};
const contextValue = useFinalContextValue(hookOptions);
const { apiNList } = _getApiDesc(name, contextValue.bridge);
useInitEffect(onInit, name, apiNList, contextValue);
return apiNList;
},
useBoundaryPayload: (hookOptions) => {
const contextValue = useFinalContextValue(hookOptions);
return contextValue.payload;
},
useBoundaryRef: () => {
return useRef(null);
},
useContextValue: (...args) => {
const payload2 = args[0];
const parent = useContext(BridgeContext);
return useMemo(() => {
return {
bridge: {},
parent,
payload: payload2
};
}, []);
},
useRegister: (name, init, deps, hookOptions) => {
const isMulti = getIsMulti(name);
const contextValue = useFinalContextValue(hookOptions);
const { apiNList } = useMemo(() => _getApiDesc(name, contextValue.bridge), [name, contextValue]);
const apiRef = useUniqueElementRef(apiNList);
const registerId = useHookId();
useImperativeHandle(apiRef, () => {
return init();
}, deps);
useEffect(() => {
let clearFns = [];
const { resolvers, resolver: initialResolver } = _getResolverOrCreateWhenMissing(apiNList, contextValue, true);
resolvers.forEach((resolver) => {
resolver.resolve(apiNList);
});
resolvers.length = 0;
resolvers.push(initialResolver);
const callbacks = cacheInitCbMap.get(apiNList);
callbacks?.filter((initInfo) => {
return !initializedOnInitMap.get(apiRef)?.includes(initInfo.hookId);
}).forEach((initInfo) => {
clearFns.push(appendToMappedValue(initializedOnInitMap, apiRef, initInfo.hookId));
const onInit = initInfo.onInit;
if (isMulti) {
const _assertedOnInit = onInit;
clearFns.push(_assertedOnInit(apiRef, apiNList));
} else {
const _assertedOnInit = onInit;
clearFns.push(_assertedOnInit(apiRef));
}
});
return () => {
clearFns.forEach(tryInvoke);
initializedOnInitMap.delete(apiRef);
};
}, []);
},
useTools: (hookOptions) => {
const contextValue = useFinalContextValue(hookOptions);
const getAPI = useCallback((_name, options) => {
return _getApiDesc(_name, options?.contextValue?.bridge || contextValue.bridge).apiNList;
}, [contextValue.bridge]);
const getBoundaryPayload = useCallback((options) => {
return (options?.contextValue || contextValue).payload;
}, [contextValue.payload]);
const getUpperAPI = useCallback((_name, options) => {
return _getUpperApiDesc(options?.contextValue || contextValue, _name, options)?.apiNList;
}, []);
const getUpperBoundaryPayload = useCallback((options) => {
const parent = _getUpperContextValue(options?.contextValue || contextValue, options?.shouldForwardYield);
if (!parent) return;
return parent.payload;
}, []);
const getAPIAsync = (_name, options) => {
const { initial } = {
initial: true,
...options
};
const { apiNList } = _getApiDesc(_name, (options?.contextValue || contextValue).bridge);
const { resolver } = _getResolverOrCreateWhenMissing(apiNList, contextValue, initial);
return resolver.promise;
};
return {
getAPI,
getBoundaryPayload,
getUpperAPI,
getUpperBoundaryPayload,
getAPIAsync
};
},
useUpperAPI: (name, hookOptions) => {
const {
onInit
} = hookOptions || {};
const contextValue = useFinalContextValue(hookOptions);
const _apiNList = useMemo(() => {
return _getUpperApiDesc(contextValue, name, hookOptions)?.apiNList;
}, []);
if (!_apiNList) return;
useInitEffect(onInit, name, _apiNList, contextValue);
return _apiNList;
},
useUpperBoundaryPayload: (hookOptions) => {
const { shouldForwardYield } = hookOptions || {};
const contextValue = useFinalContextValue(hookOptions);
const boundaryContextValue = useMemo(() => {
return _getUpperContextValue(contextValue, shouldForwardYield);
}, []);
return boundaryContextValue?.payload;
}
};
}
function useUniqueElementRef(entity) {
const elRef = useRef(null);
useEffect(() => {
if (Array.isArray(entity)) {
entity.push(elRef);
}
return () => {
removeArrayElement(entity, elRef);
};
}, [entity]);
return Array.isArray(entity) ? elRef : entity;
}
function useHookId() {
return useMemo(() => {
return true ? genHookId() : Symbol("hookId");
}, []);
}
export {
bridge_default as default
};