navigation-stack 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/.github/workflows/main.yml +39 -39
  2. package/README.md +128 -27
  3. package/lib/cjs/{LocationStateStorage.js → LocationDataStorage.js} +5 -5
  4. package/lib/cjs/addBeforeLocationChangeListener.js +7 -0
  5. package/lib/cjs/beforeLocationChangeListeners.js +51 -0
  6. package/lib/cjs/createMiddlewares.js +21 -17
  7. package/lib/cjs/index.js +9 -9
  8. package/lib/cjs/middleware/createBasePathMiddleware.js +2 -2
  9. package/lib/cjs/middleware/createBeforeLocationChangeListenerMiddleware.js +39 -0
  10. package/lib/cjs/middleware/{createEnvironmentMiddleware.js → createLocationMiddleware.js} +12 -14
  11. package/lib/cjs/middleware/createNavigationBlockerMiddleware.js +62 -29
  12. package/lib/cjs/middleware/createTransformLocationMiddleware.js +2 -2
  13. package/lib/cjs/navigationBlockers.js +55 -47
  14. package/lib/cjs/normalizeInputLocation.js +1 -0
  15. package/lib/cjs/parseLocationUrl.js +2 -0
  16. package/lib/cjs/parseQueryFromSearch.js +1 -1
  17. package/lib/cjs/session/BrowserSession.js +229 -0
  18. package/lib/cjs/session/MemorySession.js +223 -0
  19. package/lib/cjs/{environment/ServerEnvironment.js → session/ServerSession.js} +28 -16
  20. package/lib/esm/{LocationStateStorage.js → LocationDataStorage.js} +4 -4
  21. package/lib/esm/addBeforeLocationChangeListener.js +2 -0
  22. package/lib/esm/beforeLocationChangeListeners.js +44 -0
  23. package/lib/esm/createMiddlewares.js +21 -17
  24. package/lib/esm/index.js +4 -4
  25. package/lib/esm/middleware/createBasePathMiddleware.js +2 -2
  26. package/lib/esm/middleware/createBeforeLocationChangeListenerMiddleware.js +34 -0
  27. package/lib/esm/middleware/{createEnvironmentMiddleware.js → createLocationMiddleware.js} +11 -13
  28. package/lib/esm/middleware/createNavigationBlockerMiddleware.js +62 -29
  29. package/lib/esm/middleware/createTransformLocationMiddleware.js +2 -2
  30. package/lib/esm/navigationBlockers.js +55 -47
  31. package/lib/esm/normalizeInputLocation.js +1 -0
  32. package/lib/esm/parseLocationUrl.js +2 -0
  33. package/lib/esm/parseQueryFromSearch.js +1 -1
  34. package/lib/esm/session/BrowserSession.js +223 -0
  35. package/lib/esm/session/MemorySession.js +217 -0
  36. package/lib/esm/{environment/ServerEnvironment.js → session/ServerSession.js} +27 -15
  37. package/lib/index.d.ts +66 -61
  38. package/package.json +5 -5
  39. package/src/{LocationStateStorage.js → LocationDataStorage.js} +4 -4
  40. package/src/addBeforeLocationChangeListener.js +2 -0
  41. package/src/beforeLocationChangeListeners.js +54 -0
  42. package/src/createMiddlewares.js +21 -17
  43. package/src/index.js +4 -4
  44. package/src/middleware/createBasePathMiddleware.js +2 -2
  45. package/src/middleware/createBeforeLocationChangeListenerMiddleware.js +40 -0
  46. package/src/middleware/{createEnvironmentMiddleware.js → createLocationMiddleware.js} +12 -14
  47. package/src/middleware/createNavigationBlockerMiddleware.js +68 -28
  48. package/src/middleware/createTransformLocationMiddleware.js +2 -2
  49. package/src/navigationBlockers.js +68 -49
  50. package/src/normalizeInputLocation.js +1 -0
  51. package/src/parseLocationUrl.js +2 -0
  52. package/src/parseQueryFromSearch.js +1 -1
  53. package/src/session/BrowserSession.js +225 -0
  54. package/src/session/MemorySession.js +219 -0
  55. package/src/{environment/ServerEnvironment.js → session/ServerSession.js} +28 -15
  56. package/test/{LocationStateStorage.test.js → LocationDataStorage.test.js} +6 -6
  57. package/test/createMiddlewares.test.js +2 -2
  58. package/test/helpers.js +1 -1
  59. package/test/index.test.js +3 -3
  60. package/test/middleware/createBasePathMiddleware.test.js +7 -7
  61. package/test/middleware/createBeforeLocationChangeListenerMiddleware.test.js +141 -0
  62. package/test/middleware/createNavigationBlockerMiddleware.test.js +96 -97
  63. package/test/middleware/createTransformLocationMiddleware.test.js +1 -1
  64. package/test/normalizeInputLocation.test.js +3 -0
  65. package/test/parseLocationUrl.test.js +2 -0
  66. package/test/{environment/BrowserEnvironment.test.js → session/BrowserSession.test.js} +35 -18
  67. package/test/session/MemorySession.test.js +244 -0
  68. package/test/session/ServerSession.test.js +23 -0
  69. package/types/index.d.ts +64 -59
  70. package/lib/cjs/environment/BrowserEnvironment.js +0 -111
  71. package/lib/cjs/environment/MemoryEnvironment.js +0 -150
  72. package/lib/esm/environment/BrowserEnvironment.js +0 -104
  73. package/lib/esm/environment/MemoryEnvironment.js +0 -143
  74. package/src/environment/BrowserEnvironment.js +0 -109
  75. package/src/environment/MemoryEnvironment.js +0 -151
  76. package/test/environment/MemoryEnvironment.test.js +0 -218
  77. package/test/environment/ServerEnvironment.test.js +0 -23
@@ -1,16 +1,17 @@
1
1
  import createBasePathMiddleware from './middleware/createBasePathMiddleware';
2
- import createEnvironmentMiddleware from './middleware/createEnvironmentMiddleware';
2
+ import createBeforeLocationChangeListenerMiddleware from './middleware/createBeforeLocationChangeListenerMiddleware';
3
+ import createLocationMiddleware from './middleware/createLocationMiddleware';
3
4
  import createNavigationBlockerMiddleware from './middleware/createNavigationBlockerMiddleware';
4
5
  import navigationActionMiddleware from './middleware/navigationActionMiddleware';
5
6
  import normalizeInputLocationMiddleware from './middleware/normalizeInputLocationMiddleware';
6
7
 
7
- export default function createMiddlewares(environment, options) {
8
- // Allows temporarily ignoring certain environment location updates.
9
- let shouldIgnoreEnvironmentLocationUpdates = false;
10
- const ignoreEnvironmentLocationUpdates = (func) => {
11
- shouldIgnoreEnvironmentLocationUpdates = true;
8
+ export default function createMiddlewares(session, options) {
9
+ // Allows temporarily ignoring location update events.
10
+ let shouldIgnoreLocationSubscriptionEvents = false;
11
+ const ignoreLocationSubscriptionEvents = (func) => {
12
+ shouldIgnoreLocationSubscriptionEvents = true;
12
13
  func();
13
- shouldIgnoreEnvironmentLocationUpdates = false;
14
+ shouldIgnoreLocationSubscriptionEvents = false;
14
15
  };
15
16
 
16
17
  return [
@@ -23,19 +24,22 @@ export default function createMiddlewares(environment, options) {
23
24
  createBasePathMiddleware(options && options.basePath),
24
25
  // Allows blocking navigation.
25
26
  // Handles `NAVIGATE` actions dispatched by the application itself.
26
- createNavigationBlockerMiddleware(environment, {
27
- ignoreEnvironmentLocationUpdates,
27
+ createNavigationBlockerMiddleware(session, {
28
+ ignoreLocationSubscriptionEvents,
28
29
  }),
29
- // This "middleware" performs the actual navigation according to the `environment` being used.
30
- // For example, when `BrowserEnvironment` is used, it calls methods of the `history` object.
31
- createEnvironmentMiddleware(environment, {
32
- shouldIgnoreEnvironmentLocationUpdates: () =>
33
- shouldIgnoreEnvironmentLocationUpdates,
30
+ // This "middleware" performs the actual navigation according to the `session` being used.
31
+ // For example, when `BrowserSession` is used, it calls methods of the `history` object.
32
+ createLocationMiddleware(session, {
33
+ shouldIgnoreLocationSubscriptionEvents: () =>
34
+ shouldIgnoreLocationSubscriptionEvents,
34
35
  }),
35
36
  // Allows blocking navigation.
36
- // Handles location `UPDATE` actions dispatched by the environment.
37
- createNavigationBlockerMiddleware(environment, {
38
- ignoreEnvironmentLocationUpdates,
37
+ // Handles location `UPDATE` actions dispatched in response to location update events.
38
+ createNavigationBlockerMiddleware(session, {
39
+ ignoreLocationSubscriptionEvents,
39
40
  }),
41
+ // Allows subscribing to upcoming location changes
42
+ // before those changes are applied in the `location` object in the state.
43
+ createBeforeLocationChangeListenerMiddleware(session),
40
44
  ];
41
45
  }
package/src/index.js CHANGED
@@ -6,7 +6,7 @@ export getLocationUrl from './getLocationUrl';
6
6
  export parseLocationUrl from './parseLocationUrl';
7
7
  export createMiddlewares from './createMiddlewares';
8
8
  export locationReducer from './locationReducer';
9
- export LocationStateStorage from './LocationStateStorage';
10
- export BrowserEnvironment from './environment/BrowserEnvironment';
11
- export MemoryEnvironment from './environment/MemoryEnvironment';
12
- export ServerEnvironment from './environment/ServerEnvironment';
9
+ export LocationDataStorage from './LocationDataStorage';
10
+ export BrowserSession from './session/BrowserSession';
11
+ export MemorySession from './session/MemorySession';
12
+ export ServerSession from './session/ServerSession';
@@ -11,9 +11,9 @@ export default function createBasePathMiddleware(basePath) {
11
11
  return addBasePath(location, basePath);
12
12
  },
13
13
 
14
- // Transforms environment `Location` object:
14
+ // Transforms subscription `Location` object:
15
15
  // removes `basePath` from the URL.
16
- transformEnvironmentLocation: (location) => {
16
+ transformSubscriptionLocation: (location) => {
17
17
  return removeBasePath(location, basePath);
18
18
  },
19
19
  });
@@ -0,0 +1,40 @@
1
+ import ActionTypes from '../ActionTypes';
2
+ import {
3
+ getBeforeLocationChangeListeners,
4
+ removeAllBeforeLocationChangeListeners,
5
+ runBeforeLocationChangeListeners,
6
+ } from '../beforeLocationChangeListeners';
7
+
8
+ // Creates a "middleware" that calls upcoming navigation listeners.
9
+ export default function createBeforeLocationChangeListenerMiddleware(session) {
10
+ return function navigationListenerMiddleware() {
11
+ return (next) => (action) => {
12
+ const { type, payload } = action;
13
+
14
+ switch (type) {
15
+ // Trigger navigation listeners before the `location` has been updated in Redux state.
16
+ // It doesn't matter that the new location URL has already been updated in the web browser's
17
+ // address bar, or that the web browser's history has already switched to the new locaiton.
18
+ // From the application's point of view, all of that doesn't matter and even doesn't exist.
19
+ // All that exists from the application's point of view is the `location` object in the Redux state.
20
+ // Until the `location` object in the Redux state is updated, the old page is still rendered.
21
+ // The appliation is only concerned with the updates of the `location` object in the Redux state
22
+ // and completely ignores any updates to the URL in the web browser's address bar.
23
+ case ActionTypes.UPDATE:
24
+ runBeforeLocationChangeListeners(
25
+ getBeforeLocationChangeListeners(session),
26
+ payload,
27
+ );
28
+ return next(action);
29
+
30
+ // Remove any navigation listeners on `DISPOSE` event.
31
+ case ActionTypes.DISPOSE:
32
+ removeAllBeforeLocationChangeListeners(session);
33
+ return next(action);
34
+
35
+ default:
36
+ return next(action);
37
+ }
38
+ };
39
+ };
40
+ }
@@ -7,20 +7,18 @@ function updateLocation(location) {
7
7
  };
8
8
  }
9
9
 
10
- // Creates a "middleware" that performs the actual navigation according to the `environment` being used.
11
- // For example, when `BrowserProtocol` is used, it calls methods of the `history` object.
12
- // A better name for this function could be something like `createProtocolMiddleware(environment)`.
13
- // A better name for "environment" could be something like "environment".
14
- export default function createEnvironmentMiddleware(
15
- environment,
16
- { shouldIgnoreEnvironmentLocationUpdates },
10
+ // Creates a "middleware" that performs the actual navigation according to the `session` being used.
11
+ // For example, when `BrowserSession` is used, it calls methods of the `window.history` object.
12
+ export default function createLocationMiddleware(
13
+ session,
14
+ { shouldIgnoreLocationSubscriptionEvents },
17
15
  ) {
18
- return function environmentMiddleware() {
16
+ return function locationMiddleware() {
19
17
  return (next) => {
20
18
  // Whenever browser location changes,
21
19
  // perform the same changes with the internal `location` object.
22
- const unsubscribe = environment.subscribe((location) => {
23
- if (!shouldIgnoreEnvironmentLocationUpdates()) {
20
+ const unsubscribe = session.navigation.subscribe((location) => {
21
+ if (!shouldIgnoreLocationSubscriptionEvents()) {
24
22
  next(updateLocation(location));
25
23
  }
26
24
  });
@@ -30,16 +28,16 @@ export default function createEnvironmentMiddleware(
30
28
 
31
29
  switch (type) {
32
30
  case ActionTypes.INIT:
33
- return next(updateLocation(environment.init()));
31
+ return next(updateLocation(session.navigation.init()));
34
32
 
35
33
  case ActionTypes.NAVIGATE:
36
- // `environment.navigate()` doesn't trigger the `subscribe()` listener.
37
- return next(updateLocation(environment.navigate(payload)));
34
+ // `session.navigate()` doesn't trigger the `subscribe()` listener.
35
+ return next(updateLocation(session.navigation.navigate(payload)));
38
36
 
39
37
  case ActionTypes.SHIFT:
40
38
  // `shift()` will trigger the `subscribe()` listener,
41
39
  // which will call `updateLocation()`.
42
- environment.shift(payload);
40
+ session.navigation.shift(payload);
43
41
  // eslint-disable-next-line consistent-return
44
42
  return;
45
43
 
@@ -5,24 +5,22 @@ import {
5
5
  removeAllNavigationBlockers,
6
6
  runNavigationBlockers,
7
7
  } from '../navigationBlockers';
8
- import onlyAllowedOnClientSide from '../onlyAllowedOnClientSide';
9
8
 
10
9
  // Creates a "middleware" that applies navigation blockers.
11
10
  export default function createNavigationBlockerMiddleware(
12
- environment,
13
- { ignoreEnvironmentLocationUpdates },
11
+ session,
12
+ { ignoreLocationSubscriptionEvents },
14
13
  ) {
15
- // A "dummy" initial value that will be ignored.
16
- let navigationBlockersEvaluationStatus = { cancelled: false };
17
-
18
14
  function createNavigationBlockersEvaluationStatus() {
19
- onlyAllowedOnClientSide();
20
- navigationBlockersEvaluationStatus.cancelled = true;
21
- navigationBlockersEvaluationStatus = { cancelled: false };
22
- return navigationBlockersEvaluationStatus;
15
+ /* eslint-disable no-underscore-dangle */
16
+ if (session._navigationBlockersEvaluationStatus) {
17
+ session._navigationBlockersEvaluationStatus.cancelled = true;
18
+ }
19
+ session._navigationBlockersEvaluationStatus = { cancelled: false };
20
+ return session._navigationBlockersEvaluationStatus;
23
21
  }
24
22
 
25
- return function navigationListenerMiddleware() {
23
+ return function navigationBlockerMiddleware() {
26
24
  return (next) => (action) => {
27
25
  const { type, payload } = action;
28
26
 
@@ -31,11 +29,28 @@ export default function createNavigationBlockerMiddleware(
31
29
  let result;
32
30
 
33
31
  switch (type) {
34
- // Prevent or allow navigation that was initiated by the application.
32
+ // Prevent or allow navigation that was initiated by the application
33
+ // by dispatching a `.push()` or `.replace()` action.
34
+ //
35
+ // It doesn't handle `.shift()` navigation actions because it doesn't yet know
36
+ // the `location` that it's gonna `shift` to. Instead, it waits for the web browser
37
+ // to "shift" to that `location` and then reads it and rewinds the "shift"
38
+ // if it should've been blocked. That is handled in the `case ActionTypes.UPDATE` block.
39
+ //
40
+ // This type of "shifting" and then rewinding the "shift" doesn't really matter to the application at all.
41
+ // From the application's point of view, all of that doesn't matter and even doesn't exist.
42
+ // All that exists from the application's point of view is the `location` object in the Redux state.
43
+ // Until the `location` object in the Redux state is updated, the old page is still rendered.
44
+ // The appliation is only concerned with the updates of the `location` object in the Redux state
45
+ // and completely ignores any updates to the URL in the web browser's address bar.
46
+ //
35
47
  case ActionTypes.NAVIGATE:
36
48
  // `resultValue` variable name works around a stupid javascript error:
37
49
  // "Cannot redeclare block-scoped variable 'result'".
38
- result = runNavigationBlockers(getNavigationBlockers(), payload);
50
+ result = runNavigationBlockers(
51
+ getNavigationBlockers(session),
52
+ payload,
53
+ );
39
54
  if (isPromise(result)) {
40
55
  const status = createNavigationBlockersEvaluationStatus();
41
56
  // eslint-disable-next-line consistent-return
@@ -52,15 +67,37 @@ export default function createNavigationBlockerMiddleware(
52
67
  // eslint-disable-next-line consistent-return
53
68
  return;
54
69
 
55
- // Prevent or allow navigation that was initiated by the environment itself.
56
- // For example, in a web browser, it could happen when the user clicks the "Back"/"Forward" buttons.
70
+ // One can notice that this "middleware" handles both `NAVIGATE` and `UPDATE` Redux actions,
71
+ // even though a `NAVIGATE` action normally always causes a follow-up `UPDATE` Redux action.
72
+ // There's no contradiction here: if a navigation blocker should block a certain navigation,
73
+ // it will do that at the `NAVIGATION` stage and it won't get to the `UPDATE` stage, so it
74
+ // won't be called "second time" or something like that.
75
+ //
76
+ // One could ask then: Why handle `UPDATE` Redux action at all?
77
+ // The reason why it handles `UPDATE` Redux actions here is because
78
+ // `NAVIGATE` Redux actions are only emitted for programmatic "push" or "replace" navigation
79
+ // initiated by the application code, and there're other cases of navigation such as
80
+ // programmatic "shift" navigation or when the user manually clicks "Back" or "Forward" button
81
+ // in a web browser. Such "other" cases could only be handled by reacting to an `UPDATE` Redux action
82
+ // which is only emitted after the URL in the browser's address bar has changed.
83
+ //
84
+ // But there's no real drawback in reacting to an `UPDATE` Redux action "post factum" because
85
+ // from the application's point of view the address bar doesn't matter and even doesn't exist.
86
+ // All that exists from the application's point of view is the `location` object in the Redux state.
87
+ // Until the `location` object in the Redux state is updated, the old page is still rendered.
88
+ // The appliation is only concerned with the updates of the `location` object in the Redux state
89
+ // and completely ignores any updates to the URL in the web browser's address bar.
57
90
  //
58
- // In this scenario, the web browser is already at the new location, i.e. the navigation has already happened.
59
- // It could be "prevented" by rewinding back to the previous location.
91
+ // So here, the "middleware" attempts to prevent or allow navigation that has already happened
92
+ // in the web browser's address bar but hasn't yet happened in Redux state.
93
+ // For example, it could be a user clicking a "Back"/"Forward" button in their web browser.
94
+ // If such navigation should've been blocked, it will simply not update the `locaiton` object in Redux state,
95
+ // and it will also "rewind" the change of the URL in the web browser's address bar so that it's consistent
96
+ // with the `location` in Redux state.
60
97
  //
61
98
  case ActionTypes.UPDATE:
62
99
  // If no navigation blockers to run, don't do anything.
63
- if (getNavigationBlockers().length === 0) {
100
+ if (getNavigationBlockers(session).length === 0) {
64
101
  return next(action);
65
102
  }
66
103
 
@@ -70,23 +107,26 @@ export default function createNavigationBlockerMiddleware(
70
107
  return next(action);
71
108
  }
72
109
 
73
- // It's not really possible for a location to not have a `delta` property in a web browser environment.
110
+ // It's not really possible for a location to not have a `delta` property in a web browser session.
74
111
  // So this case is not something that's supposed to happen in real life.
75
- // Rather, it's a guard against an unsupported or incorrectly-implemented environment or something like that.
112
+ // Rather, it's a guard against an unsupported or incorrect session implementation or something like that.
76
113
  // If there's no `delta` property on the location, it means that the previous location can't be rewound to,
77
114
  // so it can't really "prevent" the navigation that has just happened.
78
115
  if (payload.delta === null) {
79
116
  return next(action);
80
117
  }
81
118
 
82
- result = runNavigationBlockers(getNavigationBlockers(), payload);
119
+ result = runNavigationBlockers(
120
+ getNavigationBlockers(session),
121
+ payload,
122
+ );
83
123
 
84
124
  if (isPromise(result)) {
85
125
  const status = createNavigationBlockersEvaluationStatus();
86
126
 
87
127
  // While location blockers are running, rewind to the previous location.
88
- ignoreEnvironmentLocationUpdates(() => {
89
- environment.shift(-payload.delta);
128
+ ignoreLocationSubscriptionEvents(() => {
129
+ session.navigation.shift(-payload.delta);
90
130
  });
91
131
 
92
132
  result.then((promiseResult) => {
@@ -96,8 +136,8 @@ export default function createNavigationBlockerMiddleware(
96
136
  } else if (!status.cancelled) {
97
137
  // Navigation not blocked.
98
138
  // Rewind back to the new location.
99
- ignoreEnvironmentLocationUpdates(() => {
100
- environment.shift(payload.delta);
139
+ ignoreLocationSubscriptionEvents(() => {
140
+ session.navigation.shift(payload.delta);
101
141
  });
102
142
  // Update the location.
103
143
  next(action);
@@ -105,8 +145,8 @@ export default function createNavigationBlockerMiddleware(
105
145
  });
106
146
  } else if (result) {
107
147
  // Prevent the navigation: rewind to the previous location.
108
- ignoreEnvironmentLocationUpdates(() => {
109
- environment.shift(-payload.delta);
148
+ ignoreLocationSubscriptionEvents(() => {
149
+ session.navigation.shift(-payload.delta);
110
150
  });
111
151
  } else {
112
152
  // Update the location.
@@ -117,7 +157,7 @@ export default function createNavigationBlockerMiddleware(
117
157
 
118
158
  // Remove any navigation blockers on `DISPOSE` event.
119
159
  case ActionTypes.DISPOSE:
120
- removeAllNavigationBlockers();
160
+ removeAllNavigationBlockers(session);
121
161
  return next(action);
122
162
 
123
163
  default:
@@ -3,7 +3,7 @@ import ActionTypes from '../ActionTypes';
3
3
  // Creates a "middleware" that transforms action payload (location).
4
4
  export default function createTransformLocationMiddleware({
5
5
  transformInputLocation,
6
- transformEnvironmentLocation,
6
+ transformSubscriptionLocation,
7
7
  }) {
8
8
  return function transformLocationMiddleware() {
9
9
  return (next) => (action) => {
@@ -18,7 +18,7 @@ export default function createTransformLocationMiddleware({
18
18
  case ActionTypes.UPDATE:
19
19
  return next({
20
20
  type,
21
- payload: transformEnvironmentLocation(payload),
21
+ payload: transformSubscriptionLocation(payload),
22
22
  });
23
23
 
24
24
  default:
@@ -1,67 +1,75 @@
1
- import isPromise from './isPromise';
2
- import onlyAllowedOnClientSide from './onlyAllowedOnClientSide';
3
-
4
- let navigationBlockersList = [];
1
+ /* eslint-disable no-underscore-dangle */
5
2
 
6
- let removeBeforeDestroyListener;
3
+ import isPromise from './isPromise';
7
4
 
8
- export function getNavigationBlockers() {
9
- return navigationBlockersList;
5
+ export function getNavigationBlockers(session) {
6
+ return session._navigationBlockersList || [];
10
7
  }
11
8
 
12
- function addNavigationBlockerToTheList(blocker) {
13
- onlyAllowedOnClientSide();
14
- navigationBlockersList.push(blocker);
9
+ function addNavigationBlockerToTheList(blocker, session) {
10
+ if (!session._navigationBlockersList) {
11
+ session._navigationBlockersList = [];
12
+ }
13
+ session._navigationBlockersList.push(blocker);
15
14
  }
16
15
 
17
- function removeNavigationBlockerFromTheList(blocker) {
18
- onlyAllowedOnClientSide();
19
- navigationBlockersList = navigationBlockersList.filter((_) => _ !== blocker);
16
+ function removeNavigationBlockerFromTheList(blocker, session) {
17
+ if (session._navigationBlockersList) {
18
+ session._navigationBlockersList = session._navigationBlockersList.filter(
19
+ (_) => _ !== blocker,
20
+ );
21
+ }
20
22
  }
21
23
 
22
- export function removeAllNavigationBlockers() {
23
- onlyAllowedOnClientSide();
24
- if (getNavigationBlockers().some((blocker) => blocker.beforeDestroy)) {
25
- removeBeforeDestroyListener();
26
- removeBeforeDestroyListener = undefined;
24
+ export function removeAllNavigationBlockers(session) {
25
+ if (
26
+ getNavigationBlockers(session).some((blocker) => blocker.beforeDestroy)
27
+ ) {
28
+ if (!session._removeBeforeDestroyListener) {
29
+ throw new Error(
30
+ '`_removeBeforeDestroyListener` property not found in the `session`',
31
+ );
32
+ }
33
+ session._removeBeforeDestroyListener();
34
+ session._removeBeforeDestroyListener = undefined;
27
35
  }
28
- navigationBlockersList = [];
36
+ session._navigationBlockersList = [];
29
37
  }
30
38
 
31
- // Runs the `listener` while ignoring any errors that might be thrown by it.
32
- function runNavigationBlocker({ listener }, location) {
39
+ // Runs the `blocker` while ignoring any errors that might be thrown by it.
40
+ function runNavigationBlocker({ blocker }, location) {
33
41
  let result;
34
42
  try {
35
- result = listener(location);
43
+ result = blocker(location);
36
44
  } catch (error) {
37
45
  // eslint-disable-next-line no-console
38
46
  console.warn(
39
- `Ignoring navigation blocker \`${listener.name}\` that failed with \`${error}\`.`,
47
+ `Ignoring navigation blocker \`${blocker.name}\` that failed with \`${error}\`.`,
40
48
  );
41
49
  // eslint-disable-next-line no-console
42
50
  console.error(error);
43
51
  }
44
52
 
45
- // If the listener returned a `Promise`, await for that `Promise`
53
+ // If the blocker returned a `Promise`, await for that `Promise`
46
54
  // and then return the result.
47
55
  if (isPromise(result)) {
48
56
  return result.catch((error) => {
49
57
  // eslint-disable-next-line no-console
50
58
  console.warn(
51
- `Ignoring navigation blocker \`${listener.name}\` that failed with \`${error}\`.`,
59
+ `Ignoring navigation blocker \`${blocker.name}\` that failed with \`${error}\`.`,
52
60
  );
53
61
  // eslint-disable-next-line no-console
54
62
  console.error(error);
55
63
  });
56
64
  }
57
- // The listener didn't return a `Promise`.
65
+ // The blocker didn't return a `Promise`.
58
66
  // Return the "synchronous" result.
59
67
  return result;
60
68
  }
61
69
 
62
- // Runs all listeners in order.
63
- // If any listener returns a non-`null` result, it stops and returns the result.
64
- // If there's no such listener, returns `true`.
70
+ // Runs all blockers in order.
71
+ // If any blocker returns `true`, it stops and returns the result.
72
+ // If there's no such blocker, returns `undefined`.
65
73
  export function runNavigationBlockers(navigationBlockers, toLocation) {
66
74
  if (navigationBlockers.length === 0) {
67
75
  return undefined;
@@ -91,17 +99,17 @@ export function runNavigationBlockers(navigationBlockers, toLocation) {
91
99
  }
92
100
 
93
101
  /* istanbul ignore next: not testable with Karma */
94
- function onBeforeDestroy() {
95
- const result = runNavigationBlockers(getNavigationBlockers(), null);
102
+ function onBeforeDestroy(session) {
103
+ const result = runNavigationBlockers(getNavigationBlockers(session), null);
96
104
 
97
- // If no listener returned anything, don't prevent the "unload" event.
105
+ // If no blocker returned anything, don't prevent the "unload" event.
98
106
  if (!result) {
99
107
  return undefined;
100
108
  }
101
109
 
102
110
  // Web browsers don't allow displaying a custom modal in "beforeunload" phase.
103
111
  // They only allow displaying a standard one, with the default text.
104
- // Hence, "asynchronous" listeners should be ignored.
112
+ // Hence, "asynchronous" blockers should be ignored.
105
113
  // https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event
106
114
  if (isPromise(result)) {
107
115
  return undefined;
@@ -111,48 +119,59 @@ function onBeforeDestroy() {
111
119
  return true;
112
120
  }
113
121
 
114
- export function addNavigationBlocker(environment, listener) {
115
- onlyAllowedOnClientSide();
116
-
122
+ export function addNavigationBlocker(session, blocker) {
117
123
  // All navigation blockers also run on `beforeDestroy` event.
118
124
  // If required, this could be a parameter of this function.
119
- // The rationale could be that adding `beforeunload` a listener
125
+ // The rationale could be that adding a `beforeunload` listener
120
126
  // disables web page caching in some browsers like Firefox.
121
127
  const beforeDestroy = true;
122
128
 
123
- // If it's the first "beforeDestroy" listener, add the global `onBeforeDestroy` listener.
129
+ // If it's the first "beforeDestroy" blocker, add the global `onBeforeDestroy` listener.
124
130
  //
125
131
  // Sidenote: Add the "beforeunload" event listener only as needed, as its presence
126
132
  // prevents the page from being added to the page navigation cache:
127
133
  //
128
134
  // "In Firefox, beforeunload is not compatible with the back/forward cache (bfcache):
129
- // that is, Firefox will not place pages in the bfcache if they have beforeunload listeners,
135
+ // that is, Firefox will not place pages in the bfcache if they have "beforeunload" listeners,
130
136
  // and this is bad for performance."
131
137
  //
132
138
  // https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event
133
139
  if (
134
140
  beforeDestroy &&
135
- !getNavigationBlockers().some((blocker) => blocker.beforeDestroy)
141
+ !getNavigationBlockers(session).some(
142
+ (navigationBlocker) => navigationBlocker.beforeDestroy,
143
+ )
136
144
  ) {
137
- removeBeforeDestroyListener =
138
- environment.addBeforeDestroyListener(onBeforeDestroy);
145
+ if (session._removeBeforeDestroyListener) {
146
+ throw new Error(
147
+ 'Unexpected `_removeBeforeDestroyListener` property found in the `session`',
148
+ );
149
+ }
150
+ session._removeBeforeDestroyListener = session.addBeforeDestroyListener(
151
+ () => onBeforeDestroy(session),
152
+ );
139
153
  }
140
154
 
141
- const blocker = { listener, beforeDestroy };
142
- addNavigationBlockerToTheList(blocker);
155
+ const newNavigationBlocker = { blocker, beforeDestroy };
156
+ addNavigationBlockerToTheList(newNavigationBlocker, session);
143
157
 
144
158
  return () => {
145
- removeNavigationBlockerFromTheList(blocker);
159
+ removeNavigationBlockerFromTheList(newNavigationBlocker, session);
146
160
 
147
- // If it was the last "beforeDestroy" listener, remove the global `onBeforeDestroy` listener.
161
+ // If it was the last "beforeDestroy" blocker, remove the global `onBeforeDestroy` listener.
148
162
  if (
149
163
  beforeDestroy &&
150
- !getNavigationBlockers().some(
164
+ !getNavigationBlockers(session).some(
151
165
  (navigationBlocker) => navigationBlocker.beforeDestroy,
152
166
  )
153
167
  ) {
154
- removeBeforeDestroyListener();
155
- removeBeforeDestroyListener = undefined;
168
+ if (!session._removeBeforeDestroyListener) {
169
+ throw new Error(
170
+ '`_removeBeforeDestroyListener` property not found in the `session`',
171
+ );
172
+ }
173
+ session._removeBeforeDestroyListener();
174
+ session._removeBeforeDestroyListener = undefined;
156
175
  }
157
176
  };
158
177
  }
@@ -38,6 +38,7 @@ export default function normalizeInputLocation(location) {
38
38
  // if those properties are not present.
39
39
  return {
40
40
  ...location,
41
+ query: location.query || {},
41
42
  search: location.search || '',
42
43
  hash: location.hash || '',
43
44
  };
@@ -34,6 +34,8 @@ export default function parseLocationUrl(url) {
34
34
  const query = parseQueryFromSearch(search);
35
35
  if (query) {
36
36
  location.query = query;
37
+ } else {
38
+ location.query = {};
37
39
  }
38
40
 
39
41
  return location;
@@ -8,5 +8,5 @@ export default function parseQueryFromSearch(search) {
8
8
  // Ignore any query parsing errors.
9
9
  }
10
10
  }
11
- return undefined;
11
+ return {};
12
12
  }