1import * as React from 'react'; 2import { Animated } from 'react-native'; 3 4export type StackAction = 'pushstart' | 'pushend' | 'popstart' | 'popend'; 5export type Status = 'pushing' | 'popping' | 'settled' | 'popped'; 6 7export type StackItemComponent<T = any> = React.JSXElementConstructor<T>; 8 9export type StackEvent<T = any> = { 10 state: StackState<T>; 11 event: { 12 action: StackAction; 13 key: string; 14 }; 15}; 16 17const AValue = new Animated.Value(0); 18 19export type StackItem<T = any> = { 20 key: string; 21 status: Status; 22 promise: Promise<StackItem<T>>; 23 pop: () => void; 24 onPushEnd: () => void; 25 onPopEnd: () => void; 26 data: T; 27 animatedValue: typeof AValue; 28}; 29 30export type StackState<T = any> = { 31 items: StackItem<T>[]; 32 lookup: Record<string, StackItem<T>>; 33 getItemByKey: (key: string) => StackItem<T> | null; 34}; 35 36export type Stack<T> = { 37 push: (data?: T | undefined) => StackItem<T>; 38 pop: (amount?: number, startIndex?: number) => StackItem<any>[]; 39 subscribe: (listener: (state: StackEvent<T>) => void) => () => void; 40 getState: () => StackState; 41}; 42 43export function createAsyncStack<T = any>() { 44 let keys: string[] = []; 45 const lookup: Record<string, StackItem<T>> = {}; 46 let count = 0; 47 48 const pushResolvers: Record<string, any> = {}; 49 const popResolvers: Record<string, Function> = {}; 50 51 let listeners: any[] = []; 52 53 function push(data?: T) { 54 count += 1; 55 const key = '' + count; 56 57 keys.push(key); 58 59 const promise = new Promise<StackItem<T>>((resolve) => { 60 pushResolvers[key] = resolve; 61 }); 62 63 const item: StackItem<T> = { 64 key, 65 promise, 66 // @ts-ignore 67 data, 68 status: 'pushing' as Status, 69 pop: () => pop(`${key}`), 70 onPushEnd: () => onPushEnd(key), 71 onPopEnd: () => onPopEnd(key), 72 animatedValue: new Animated.Value(0), 73 }; 74 75 if (data) { 76 item.data = data; 77 } 78 79 lookup[key] = item; 80 81 emit('pushstart', key); 82 83 return item; 84 } 85 86 function onPushEnd(key: string) { 87 const item = lookup[key]; 88 89 if (item.status === 'pushing') { 90 item.status = 'settled'; 91 92 const resolver = pushResolvers[key]; 93 94 if (resolver) { 95 resolver(getItemByKey(key)); 96 delete pushResolvers[key]; 97 } 98 99 emit('pushend', key); 100 } 101 102 return item; 103 } 104 105 function pop(amount: number | string = 1) { 106 const items: StackItem[] = []; 107 108 if (typeof amount === 'string') { 109 const key = amount; 110 const item = lookup[key]; 111 112 if (item) { 113 if (item.status === 'pushing') { 114 onPushEnd(key); 115 } 116 117 item.status = 'popping'; 118 119 const promise = new Promise<StackItem<T>>((resolve) => { 120 popResolvers[key] = resolve; 121 }); 122 123 item.promise = promise; 124 125 emit('popstart', key); 126 items.push(item); 127 } 128 129 return items; 130 } 131 132 if (amount === -1) { 133 // pop them all 134 amount = keys.length; 135 } 136 137 let startIndex = keys.length - 1; 138 139 for (let i = keys.length - 1; i >= 0; i--) { 140 const key = keys[i]; 141 const item = lookup[key]; 142 143 if (item && (item.status === 'settled' || item.status === 'pushing')) { 144 startIndex = i; 145 break; 146 } 147 } 148 149 for (let i = startIndex; i > startIndex - amount; i--) { 150 const key = keys[i]; 151 const item = lookup[key]; 152 153 if (item) { 154 if (item.status === 'pushing') { 155 onPushEnd(key); 156 } 157 158 item.status = 'popping'; 159 160 const promise = new Promise<StackItem<T>>((resolve) => { 161 popResolvers[key] = resolve; 162 }); 163 164 item.promise = promise; 165 166 emit('popstart', key); 167 items.push(item); 168 } 169 } 170 171 return items; 172 } 173 174 function onPopEnd(key: string) { 175 const item = lookup[key]; 176 keys = keys.filter((k) => k !== key); 177 178 const resolver = popResolvers[key]; 179 180 if (resolver) { 181 resolver(getItemByKey(key)); 182 delete popResolvers[key]; 183 } 184 185 item.status = 'popped'; 186 emit('popend', key); 187 188 return item; 189 } 190 191 function subscribe(listener: (state: StackEvent<T>) => void) { 192 listeners.push(listener); 193 194 return () => { 195 listeners = listeners.filter((l) => l !== listener); 196 }; 197 } 198 199 function emit(action: StackAction, key: string) { 200 listeners.forEach((listener) => { 201 const state = getState(); 202 const event = { key, action }; 203 listener({ state, event }); 204 }); 205 } 206 207 function getItemByKey(key: string) { 208 return lookup[key]; 209 } 210 211 function getState(): StackState { 212 const items = keys.map((key) => lookup[key]); 213 214 return { 215 items, 216 lookup, 217 getItemByKey, 218 }; 219 } 220 221 return { 222 push, 223 pop, 224 subscribe, 225 getState, 226 }; 227} 228 229export function useStackItems<T>(stack: Stack<T>) { 230 const [items, setItems] = React.useState<StackItem<T>[]>(stack.getState().items); 231 232 React.useEffect(() => { 233 const unsubscribe = stack.subscribe(({ state }) => { 234 setItems(state.items); 235 }); 236 237 return () => unsubscribe(); 238 }, []); 239 240 return items; 241} 242