1import * as React from 'react';
2
3// utility function for capturing all push and pop stack events
4// components can subscribe to internal state to push and pop their own views depending on use case
5// e.g a modal stack, a screen stack, a toast stack, etc
6
7export type IStackEvent = 'pushstart' | 'pushend' | 'popstart' | 'popend' | 'replace';
8
9export type StackItemStatus = 'pushing' | 'popping' | 'settled';
10
11export type ListenerFn<T> = ({
12  items,
13}: {
14  action: IStackEvent;
15  key: string;
16  items: StackItem<T>[];
17  getItemByKey: (key: string) => T | undefined;
18}) => void;
19
20export type StackItem<T> = T & { key: string; status: StackItemStatus };
21export type IReplaceOptions<T> = T & { replaceAmount?: number; key?: string };
22export type IPushOptions<T> = T & { key?: string };
23export interface IStack<T> {
24  push: (pushOptions: IPushOptions<T>) => Promise<string>;
25  pop: (amount?: number) => Promise<string[]>;
26  replace: (replaceOptions: IReplaceOptions<T>) => Promise<any>;
27  onPushEnd: (key: string) => void;
28  onPopEnd: (key: string) => void;
29  subscribe: (listener: ListenerFn<T>) => () => void;
30  getState: () => {
31    items: StackItem<T>[];
32    getItemByKey: (key: string) => T | undefined;
33  };
34}
35
36const generateRouteKey = () => `${new Date().getTime()}`;
37
38export function createAsyncStack<T>(): IStack<T> {
39  let keys: string[] = [];
40  const lookup: Record<string, StackItem<T>> = {};
41
42  const pushResolvers: Record<string, Function> = {};
43  const popResolvers: Record<string, Function> = {};
44
45  let listeners: any[] = [];
46
47  function push(pushOptions: IPushOptions<T>) {
48    const key = pushOptions.key || generateRouteKey();
49
50    if (keys.includes(key)) {
51      return Promise.resolve(key);
52    }
53
54    keys.push(key);
55
56    lookup[key] = {
57      ...pushOptions,
58      key,
59      status: 'pushing',
60    };
61
62    const promise = new Promise<string>((resolve) => {
63      pushResolvers[key] = resolve;
64    });
65
66    emit('pushstart', key);
67
68    return promise;
69  }
70
71  function onPushEnd(key: string) {
72    const item = lookup[key];
73
74    if (item) {
75      if (item.status === 'pushing') {
76        item.status = 'settled';
77      }
78
79      emit('pushend', key);
80
81      const resolver = pushResolvers[key];
82
83      if (resolver) {
84        resolver(key);
85      }
86    }
87  }
88
89  function pop(amount = 1, startIndex = 0) {
90    const promises = [];
91
92    if (amount === -1) {
93      // pop them all
94      amount = keys.length;
95    }
96
97    for (let i = 1; i <= amount; i++) {
98      const key = keys[keys.length - startIndex - i];
99      const item = lookup[key];
100
101      if (item) {
102        item.status = 'popping';
103
104        const promise = new Promise((resolve) => {
105          popResolvers[key] = resolve;
106        });
107
108        promises.push(promise);
109        emit('popstart', key);
110      }
111    }
112
113    return Promise.all(promises) as Promise<string[]>;
114  }
115
116  function onPopEnd(key: string) {
117    keys = keys.filter((k) => k !== key);
118
119    const resolver = popResolvers[key];
120
121    if (resolver) {
122      resolver(key);
123    }
124
125    delete popResolvers[key];
126    delete pushResolvers[key];
127
128    emit('popend', key);
129  }
130
131  async function replace(replaceOptions: IReplaceOptions<T>) {
132    const itemsToPop = replaceOptions.replaceAmount != null ? replaceOptions.replaceAmount : 1;
133
134    const promise2 = await push(replaceOptions);
135    const promise1 = await pop(itemsToPop, 1);
136
137    return Promise.all([promise2, promise1]);
138  }
139
140  function subscribe(listener: any) {
141    listeners.push(listener);
142
143    return () => {
144      listeners = listeners.filter((l) => l !== listener);
145    };
146  }
147
148  function emit(action: IStackEvent, key: string) {
149    listeners.forEach((listener) => {
150      const state = getState();
151      listener({ ...state, key, action });
152    });
153  }
154
155  function getItemByKey(key: string) {
156    return lookup[key];
157  }
158
159  function getState() {
160    const items = keys.map((key) => lookup[key]);
161
162    return {
163      items,
164      getItemByKey,
165    };
166  }
167
168  return {
169    push,
170    onPushEnd,
171    pop,
172    onPopEnd,
173    replace,
174    subscribe,
175    getState,
176  };
177}
178
179export function useStackItems<T>(stack: IStack<T>) {
180  const [items, setItems] = React.useState(() => stack.getState().items);
181
182  React.useEffect(() => {
183    const unsubscribe = stack.subscribe(({ items }) => {
184      setItems(items);
185    });
186
187    return () => {
188      unsubscribe && unsubscribe();
189    };
190  }, [stack]);
191
192  return items;
193}
194