windowresolver.ts 5.7 KB

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