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