forked from jlalmes/trpc-chrome
-
Notifications
You must be signed in to change notification settings - Fork 11
/
popup.ts
105 lines (98 loc) · 3.48 KB
/
popup.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import type { TRPCLink } from '@trpc/client';
import type { AnyRouter } from '@trpc/server';
import { TRPC_BROWSER_LOADED_EVENT } from '../shared/constants';
import type { MinimalPopupWindow, MinimalWindow, TRPCChromeMessage } from '../types';
import { createBaseLink } from './internal/base';
export type PopupLinkOptions = {
createPopup: () => MinimalPopupWindow;
listenWindow: MinimalWindow;
postOrigin?: string;
};
export const popupLink = <TRouter extends AnyRouter>(opts: PopupLinkOptions): TRPCLink<TRouter> => {
const messageHandlerMap = new Map<
(message: TRPCChromeMessage) => void,
(ev: MessageEvent<TRPCChromeMessage>) => void
>();
const closeHandlerSet = new Set<() => void>();
let popupWindow: MinimalPopupWindow | null = null;
async function getPopup(loadListenWindow: MinimalWindow) {
if (!popupWindow || popupWindow.closed) {
popupWindow = opts.createPopup();
await Promise.race([
// wait til window is loaded (same origin)
new Promise((resolve) => {
try {
popupWindow?.addEventListener?.('load', resolve);
} catch {
// if this throws, it's a cross-origin popup and should stay pending (never resolve)
}
}),
// this is needed for cross-origin popups as they don't have a load event
new Promise<void>((resolve) => {
loadListenWindow.addEventListener('message', (event) => {
if (event.data === TRPC_BROWSER_LOADED_EVENT) {
resolve();
}
});
}),
// expect the popup to load after 15s max, in case non of the above events fire
new Promise((resolve) => {
console.warn(
'Could not detect if popup loading succeeded after 15s timeout, continuing anyway',
);
setTimeout(resolve, 15000);
}),
]);
// subscribe to popup closing
try {
if (!popupWindow.addEventListener) {
throw new Error('popupWindow.addEventListener is not a function');
}
popupWindow.addEventListener('beforeunload', () => {
popupWindow = null;
});
} catch {
// this throws on cross-origin popups, fallback to polling to check if popup is closed
const pid = setInterval(() => {
if (popupWindow && popupWindow.closed) {
popupWindow = null;
closeHandlerSet.forEach((handler) => {
handler();
});
clearInterval(pid);
}
}, 1000);
}
}
return popupWindow;
}
return createBaseLink({
async postMessage(message) {
const popup = await getPopup(opts.listenWindow);
return popup.postMessage(message, {
targetOrigin: opts.postOrigin,
});
},
addMessageListener(listener) {
const handler = (ev: MessageEvent<TRPCChromeMessage>) => {
listener(ev.data);
};
messageHandlerMap.set(listener, handler);
opts.listenWindow.addEventListener('message', handler);
},
removeMessageListener(listener) {
const handler = messageHandlerMap.get(listener);
if (handler) {
opts.listenWindow.removeEventListener('message', handler);
}
},
addCloseListener(listener) {
opts.listenWindow.addEventListener('beforeunload', listener);
closeHandlerSet.add(listener);
},
removeCloseListener(listener) {
opts.listenWindow.removeEventListener('beforeunload', listener);
closeHandlerSet.delete(listener);
},
});
};