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