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