1 | // Copyright (c) Jupyter Development Team.
|
2 | // Distributed under the terms of the Modified BSD License.
|
3 | import { PromiseDelegate } from '@lumino/coreutils';
|
4 | /**
|
5 | * A concrete implementation of a window name resolver.
|
6 | */
|
7 | export 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 | */
|
38 | var 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 |