import * as React from 'react'; import { Animated } from 'react-native'; export type StackAction = 'pushstart' | 'pushend' | 'popstart' | 'popend'; export type Status = 'pushing' | 'popping' | 'settled' | 'popped'; export type StackItemComponent = React.JSXElementConstructor; export type StackEvent = { state: StackState; event: { action: StackAction; key: string; }; }; const AValue = new Animated.Value(0); export type StackItem = { key: string; status: Status; promise: Promise>; pop: () => void; onPushEnd: () => void; onPopEnd: () => void; data: T; animatedValue: typeof AValue; }; export type StackState = { items: StackItem[]; lookup: Record>; getItemByKey: (key: string) => StackItem | null; }; export type Stack = { push: (data?: T | undefined) => StackItem; pop: (amount?: number, startIndex?: number) => StackItem[]; subscribe: (listener: (state: StackEvent) => void) => () => void; getState: () => StackState; }; export function createAsyncStack() { let keys: string[] = []; const lookup: Record> = {}; let count = 0; const pushResolvers: Record = {}; const popResolvers: Record = {}; let listeners: any[] = []; function push(data?: T) { count += 1; const key = '' + count; keys.push(key); const promise = new Promise>((resolve) => { pushResolvers[key] = resolve; }); const item: StackItem = { key, promise, // @ts-ignore data, status: 'pushing' as Status, pop: () => pop(`${key}`), onPushEnd: () => onPushEnd(key), onPopEnd: () => onPopEnd(key), animatedValue: new Animated.Value(0), }; if (data) { item.data = data; } lookup[key] = item; emit('pushstart', key); return item; } function onPushEnd(key: string) { const item = lookup[key]; if (item.status === 'pushing') { item.status = 'settled'; const resolver = pushResolvers[key]; if (resolver) { resolver(getItemByKey(key)); delete pushResolvers[key]; } emit('pushend', key); } return item; } function pop(amount: number | string = 1) { const items: StackItem[] = []; if (typeof amount === 'string') { const key = amount; const item = lookup[key]; if (item) { if (item.status === 'pushing') { onPushEnd(key); } item.status = 'popping'; const promise = new Promise>((resolve) => { popResolvers[key] = resolve; }); item.promise = promise; emit('popstart', key); items.push(item); } return items; } if (amount === -1) { // pop them all amount = keys.length; } let startIndex = keys.length - 1; for (let i = keys.length - 1; i >= 0; i--) { const key = keys[i]; const item = lookup[key]; if (item && (item.status === 'settled' || item.status === 'pushing')) { startIndex = i; break; } } for (let i = startIndex; i > startIndex - amount; i--) { const key = keys[i]; const item = lookup[key]; if (item) { if (item.status === 'pushing') { onPushEnd(key); } item.status = 'popping'; const promise = new Promise>((resolve) => { popResolvers[key] = resolve; }); item.promise = promise; emit('popstart', key); items.push(item); } } return items; } function onPopEnd(key: string) { const item = lookup[key]; keys = keys.filter((k) => k !== key); const resolver = popResolvers[key]; if (resolver) { resolver(getItemByKey(key)); delete popResolvers[key]; } item.status = 'popped'; emit('popend', key); return item; } function subscribe(listener: (state: StackEvent) => void) { listeners.push(listener); return () => { listeners = listeners.filter((l) => l !== listener); }; } function emit(action: StackAction, key: string) { listeners.forEach((listener) => { const state = getState(); const event = { key, action }; listener({ state, event }); }); } function getItemByKey(key: string) { return lookup[key]; } function getState(): StackState { const items = keys.map((key) => lookup[key]); return { items, lookup, getItemByKey, }; } return { push, pop, subscribe, getState, }; } export function useStackItems(stack: Stack) { const [items, setItems] = React.useState[]>(stack.getState().items); React.useEffect(() => { const unsubscribe = stack.subscribe(({ state }) => { setItems(state.items); }); return () => unsubscribe(); }, []); return items; }