UNPKG

@ryo-98/react-api-bridge

Version:
407 lines (404 loc) 12.9 kB
// ../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 };