import * as lithentCore from 'lithent'; /** * DataStore */ export type StoreRenew = (store: T) => boolean | AbortSignal | void; export type StoreType = V extends { [key: string]: unknown } ? V : { value: V }; export type StoreObserver = (store: T) => unknown[]; export type StoreOptions = { cache?: boolean }; type Run = () => boolean | AbortSignal | void; type StoreValue = { [key: string | symbol]: Set; }; const DEFAULT_OPTION = { cache: true }; /** * 리액티브 스토어 생성 */ export function store(initialValue: V) { type T = StoreType; const allowInitSetting = { value: false }; const isObjectTypeValue = !Array.isArray(initialValue) && typeof initialValue === 'object' && initialValue !== null; const value: T = isObjectTypeValue ? (initialValue as T) : ({ value: initialValue } as T); const storeRenderList: Set = new Set(); const storeRenderObserveList: StoreValue[] = []; const cacheMap = new WeakMap, T>(); return ( renew?: StoreRenew, makeObserver?: StoreObserver | null, userOption?: StoreOptions ) => { const { cache } = Object.assign({}, DEFAULT_OPTION, userOption || {}); if (cache && renew && cacheMap.has(renew)) { return cacheMap.get(renew)!; } const storeRenderObserveMap: StoreValue = {}; const allowedAccessProp: Set = new Set(); let makedProxy: { value: null | T } = { value: null }; let run: Run = () => {}; storeRenderObserveList.push(storeRenderObserveMap); if (renew && makeObserver) { run = () => renew(makedProxy.value!); makedProxy.value = updater( value, allowInitSetting, storeRenderList, allowedAccessProp, storeRenderObserveList, run, storeRenderObserveMap ); allowInitSetting.value = true; makeObserver(makedProxy.value); allowInitSetting.value = false; } if (!makedProxy.value) { makedProxy.value = updater( value, allowInitSetting, storeRenderList, allowedAccessProp, storeRenderObserveList ); if (renew) { run = () => renew(makedProxy.value!); storeRenderList.add(run); } } if (renew) { runFirstEmit( run, storeRenderList, storeRenderObserveMap, allowedAccessProp ); cacheMap.set(renew, makedProxy.value); } return makedProxy.value; }; } /** * Tells a concurrent-capable core that store data moved (D6, DC-5). * * Deliberately a namespace lookup rather than a named import: this file has to * keep building and running against the frozen base core, which does not export * `notifyStoreWrite`. A named import would fail to link there; a missing * property on a module namespace is simply `undefined`, so on the base core * this is a no-op and the store behaves exactly as it always did. */ const notifyStoreWrite = () => (lithentCore as { notifyStoreWrite?: () => void }).notifyStoreWrite?.(); /** * 사용자에게 노출될 프록시 개체 생성 */ function updater( value: T, allowInitSetting: { value: boolean }, storeRenderList: Set, allowedAccessProp: Set, storeRenderObserveList: StoreValue[], run?: Run, storeRenderObserveMap?: StoreValue ) { const result = new Proxy(value, { get(target: T, prop: keyof T) { if (run && storeRenderObserveMap && allowInitSetting.value) { storeRenderObserveMap[prop] ??= new Set(); if (!storeRenderObserveMap[prop].has(run)) { storeRenderObserveMap[prop].add(run); allowedAccessProp.add(prop); } } return target[prop]; }, set(target, prop: keyof T, value) { if (target[prop] === value) { return true; } target[prop] = value; notifyStoreWrite(); execDependentCallbacks(storeRenderList, storeRenderObserveList, prop); return true; }, }); return result; } /** * 값 변경시 의존성이 있는 콜백을 찾아서 실행 */ function execDependentCallbacks( storeRenderList: Set, storeRenderObserveList: StoreValue[] = [], prop: keyof T ) { const trashCollections: Set = new Set(); // trashCollections.push(...runWithtrashCollectUnit(storeRenderList)); runWithtrashCollectUnit(storeRenderList).forEach(value => trashCollections.add(value) ); (storeRenderObserveList || []).forEach(storeRenderObserveMap => { const renderObserveList: Set = storeRenderObserveMap[prop] || new Set(); runWithtrashCollectUnit(renderObserveList).forEach(value => trashCollections.add(value) ); removeTrashCollect(trashCollections, renderObserveList); }); removeTrashCollect(trashCollections, storeRenderList); } /** * 의미 없어진 구독 콜백 제거 */ function removeTrashCollect(trashCollections: Set, targetList: Set) { trashCollections.forEach(deleteTarget => { targetList.delete(deleteTarget); }); } /** * 단발성 구독 함수는 한번 실행 하고 제거 */ function runWithtrashCollectUnit(storeRenderList: Set) { const trashes: Run[] = []; storeRenderList.forEach(run => { if (run() === false) { trashes.push(run); } }); return trashes; } /** * 구독이 처음 실행되면 abort 이벤트 수집하여 바인딩 */ function runFirstEmit( run: () => boolean | void | AbortSignal, storeRenderList: Set, storeRenderObserveMap: StoreValue, allowedAccessProp: Set ) { const renewResult = run(); if (renewResult instanceof AbortSignal) { renewResult.addEventListener('abort', () => { const renderObserveList: StoreValue = storeRenderObserveMap || {}; storeRenderList.delete(run); Object.entries(renderObserveList).forEach(([key, item]) => { item.delete(run); allowedAccessProp.delete(key as keyof T); }); }); } }