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