UNPKG

5.84 kBJavaScriptView Raw
1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3import { PromiseDelegate } from '@lumino/coreutils';
4/**
5 * A concrete implementation of a window name resolver.
6 */
7export class WindowResolver {
8 /**
9 * The resolved window name.
10 *
11 * #### Notes
12 * If the `resolve` promise has not resolved, the behavior is undefined.
13 */
14 get name() {
15 return this._name;
16 }
17 /**
18 * Resolve a window name to use as a handle among shared resources.
19 *
20 * @param candidate - The potential window name being resolved.
21 *
22 * #### Notes
23 * Typically, the name candidate should be a JupyterLab workspace name or
24 * an empty string if there is no workspace.
25 *
26 * If the returned promise rejects, a window name cannot be resolved without
27 * user intervention, which typically means navigation to a new URL.
28 */
29 resolve(candidate) {
30 return Private.resolve(candidate).then(name => {
31 this._name = name;
32 });
33 }
34}
35/*
36 * A namespace for private module data.
37 */
38var Private;
39(function (Private) {
40 /**
41 * The internal prefix for private local storage keys.
42 */
43 const PREFIX = '@jupyterlab/statedb:StateDB';
44 /**
45 * The local storage beacon key.
46 */
47 const BEACON = `${PREFIX}:beacon`;
48 /**
49 * The timeout (in ms) to wait for beacon responders.
50 *
51 * #### Notes
52 * This value is a whole number between 200 and 500 in order to prevent
53 * perfect timeout collisions between multiple simultaneously opening windows
54 * that have the same URL. This is an edge case because multiple windows
55 * should not ordinarily share the same URL, but it can be contrived.
56 */
57 const TIMEOUT = Math.floor(200 + Math.random() * 300);
58 /**
59 * The local storage window key.
60 */
61 const WINDOW = `${PREFIX}:window`;
62 /**
63 * Current beacon request
64 *
65 * #### Notes
66 * We keep track of the current request so that we can ignore our own beacon
67 * requests. This is to work around a bug in Safari, where Safari sometimes
68 * triggers local storage events for changes made by the current tab. See
69 * https://github.com/jupyterlab/jupyterlab/issues/6921#issuecomment-540817283
70 * for more details.
71 */
72 let currentBeaconRequest = null;
73 /**
74 * A potential preferred default window name.
75 */
76 let candidate = null;
77 /**
78 * The window name promise.
79 */
80 const delegate = new PromiseDelegate();
81 /**
82 * The known window names.
83 */
84 const known = {};
85 /**
86 * The window name.
87 */
88 let name = null;
89 /**
90 * Whether the name resolution has completed.
91 */
92 let resolved = false;
93 /**
94 * Start the storage event handler.
95 */
96 function initialize() {
97 // Listen to all storage events for beacons and window names.
98 window.addEventListener('storage', (event) => {
99 const { key, newValue } = event;
100 // All the keys we care about have values.
101 if (newValue === null) {
102 return;
103 }
104 // If the beacon was fired, respond with a ping.
105 if (key === BEACON &&
106 newValue !== currentBeaconRequest &&
107 candidate !== null) {
108 ping(resolved ? name : candidate);
109 return;
110 }
111 // If the window name is resolved, bail.
112 if (resolved || key !== WINDOW) {
113 return;
114 }
115 const reported = newValue.replace(/\-\d+$/, '');
116 // Store the reported window name.
117 known[reported] = null;
118 // If a reported window name and candidate collide, reject the candidate.
119 if (!candidate || candidate in known) {
120 reject();
121 }
122 });
123 }
124 /**
125 * Ping peers with payload.
126 */
127 function ping(payload) {
128 if (payload === null) {
129 return;
130 }
131 const { localStorage } = window;
132 localStorage.setItem(WINDOW, `${payload}-${new Date().getTime()}`);
133 }
134 /**
135 * Reject the candidate.
136 */
137 function reject() {
138 resolved = true;
139 currentBeaconRequest = null;
140 delegate.reject(`Window name candidate "${candidate}" already exists`);
141 }
142 /**
143 * Returns a promise that resolves with the window name used for restoration.
144 */
145 function resolve(potential) {
146 if (resolved) {
147 return delegate.promise;
148 }
149 // Set the local candidate.
150 candidate = potential;
151 if (candidate in known) {
152 reject();
153 return delegate.promise;
154 }
155 const { localStorage, setTimeout } = window;
156 // Wait until other windows have reported before claiming the candidate.
157 setTimeout(() => {
158 if (resolved) {
159 return;
160 }
161 // If the window name has not already been resolved, check one last time
162 // to confirm it is not a duplicate before resolving.
163 if (!candidate || candidate in known) {
164 return reject();
165 }
166 resolved = true;
167 currentBeaconRequest = null;
168 delegate.resolve((name = candidate));
169 ping(name);
170 }, TIMEOUT);
171 // Fire the beacon to collect other windows' names.
172 currentBeaconRequest = `${Math.random()}-${new Date().getTime()}`;
173 localStorage.setItem(BEACON, currentBeaconRequest);
174 return delegate.promise;
175 }
176 Private.resolve = resolve;
177 // Initialize the storage listener at runtime.
178 (() => {
179 initialize();
180 })();
181})(Private || (Private = {}));
182//# sourceMappingURL=windowresolver.js.map
\No newline at end of file