import * as React from 'react'; // utility function for capturing all push and pop stack events // components can subscribe to internal state to push and pop their own views depending on use case // e.g a modal stack, a screen stack, a toast stack, etc export type IStackEvent = 'pushstart' | 'pushend' | 'popstart' | 'popend' | 'replace'; export type StackItemStatus = 'pushing' | 'popping' | 'settled'; export type ListenerFn = ({ items, }: { action: IStackEvent; key: string; items: StackItem[]; getItemByKey: (key: string) => T | undefined; }) => void; export type StackItem = T & { key: string; status: StackItemStatus }; export type IReplaceOptions = T & { replaceAmount?: number; key?: string }; export type IPushOptions = T & { key?: string }; export interface IStack { push: (pushOptions: IPushOptions) => Promise; pop: (amount?: number) => Promise; replace: (replaceOptions: IReplaceOptions) => Promise; onPushEnd: (key: string) => void; onPopEnd: (key: string) => void; subscribe: (listener: ListenerFn) => () => void; getState: () => { items: StackItem[]; getItemByKey: (key: string) => T | undefined; }; } const generateRouteKey = () => `${new Date().getTime()}`; export function createAsyncStack(): IStack { let keys: string[] = []; const lookup: Record> = {}; const pushResolvers: Record = {}; const popResolvers: Record = {}; let listeners: any[] = []; function push(pushOptions: IPushOptions) { const key = pushOptions.key || generateRouteKey(); if (keys.includes(key)) { return Promise.resolve(key); } keys.push(key); lookup[key] = { ...pushOptions, key, status: 'pushing', }; const promise = new Promise((resolve) => { pushResolvers[key] = resolve; }); emit('pushstart', key); return promise; } function onPushEnd(key: string) { const item = lookup[key]; if (item) { if (item.status === 'pushing') { item.status = 'settled'; } emit('pushend', key); const resolver = pushResolvers[key]; if (resolver) { resolver(key); } } } function pop(amount = 1, startIndex = 0) { const promises = []; if (amount === -1) { // pop them all amount = keys.length; } for (let i = 1; i <= amount; i++) { const key = keys[keys.length - startIndex - i]; const item = lookup[key]; if (item) { item.status = 'popping'; const promise = new Promise((resolve) => { popResolvers[key] = resolve; }); promises.push(promise); emit('popstart', key); } } return Promise.all(promises) as Promise; } function onPopEnd(key: string) { keys = keys.filter((k) => k !== key); const resolver = popResolvers[key]; if (resolver) { resolver(key); } delete popResolvers[key]; delete pushResolvers[key]; emit('popend', key); } async function replace(replaceOptions: IReplaceOptions) { const itemsToPop = replaceOptions.replaceAmount != null ? replaceOptions.replaceAmount : 1; const promise2 = await push(replaceOptions); const promise1 = await pop(itemsToPop, 1); return Promise.all([promise2, promise1]); } function subscribe(listener: any) { listeners.push(listener); return () => { listeners = listeners.filter((l) => l !== listener); }; } function emit(action: IStackEvent, key: string) { listeners.forEach((listener) => { const state = getState(); listener({ ...state, key, action }); }); } function getItemByKey(key: string) { return lookup[key]; } function getState() { const items = keys.map((key) => lookup[key]); return { items, getItemByKey, }; } return { push, onPushEnd, pop, onPopEnd, replace, subscribe, getState, }; } export function useStackItems(stack: IStack) { const [items, setItems] = React.useState(() => stack.getState().items); React.useEffect(() => { const unsubscribe = stack.subscribe(({ items }) => { setItems(items); }); return () => { unsubscribe && unsubscribe(); }; }, [stack]); return items; }