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      item.status = 'settled';
76
77      emit('pushend', key);
78
79      const resolver = pushResolvers[key];
80
81      if (resolver) {
82        resolver(key);
83      }
84    }
85  }
86
87  function pop(amount = 1, startIndex = 0) {
88    const promises = [];
89
90    if (amount === -1) {
91      // pop them all
92      amount = keys.length;
93    }
94
95    for (let i = 1; i <= amount; i++) {
96      const key = keys[keys.length - startIndex - i];
97      const item = lookup[key];
98
99      if (item) {
100        item.status = 'popping';
101
102        const promise = new Promise((resolve) => {
103          popResolvers[key] = resolve;
104        });
105
106        promises.push(promise);
107        emit('popstart', key);
108      }
109    }
110
111    return Promise.all(promises) as Promise<string[]>;
112  }
113
114  function onPopEnd(key: string) {
115    keys = keys.filter((k) => k !== key);
116
117    const resolver = popResolvers[key];
118
119    if (resolver) {
120      resolver(key);
121    }
122
123    delete popResolvers[key];
124    delete pushResolvers[key];
125
126    emit('popend', key);
127  }
128
129  async function replace(replaceOptions: IReplaceOptions<T>) {
130    const itemsToPop = replaceOptions.replaceAmount != null ? replaceOptions.replaceAmount : 1;
131
132    const promise2 = await push(replaceOptions);
133    const promise1 = await pop(itemsToPop, 1);
134
135    return Promise.all([promise2, promise1]);
136  }
137
138  function subscribe(listener: any) {
139    listeners.push(listener);
140
141    return () => {
142      listeners = listeners.filter((l) => l !== listener);
143    };
144  }
145
146  function emit(action: IStackEvent, key: string) {
147    listeners.forEach((listener) => {
148      const state = getState();
149      listener({ ...state, key, action });
150    });
151  }
152
153  function getItemByKey(key: string) {
154    return lookup[key];
155  }
156
157  function getState() {
158    const items = keys.map((key) => lookup[key]);
159
160    return {
161      items,
162      getItemByKey,
163    };
164  }
165
166  return {
167    push,
168    onPushEnd,
169    pop,
170    onPopEnd,
171    replace,
172    subscribe,
173    getState,
174  };
175}
176
177export function useStackItems<T>(stack: IStack<T>) {
178  const [items, setItems] = React.useState(() => stack.getState().items);
179
180  React.useEffect(() => {
181    const unsubscribe = stack.subscribe(({ items }) => {
182      setItems(items);
183    });
184
185    return () => {
186      unsubscribe && unsubscribe();
187    };
188  }, [stack]);
189
190  return items;
191}
192