navigation-stack 0.5.2 → 0.6.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 (210) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +144 -282
  3. package/karma.conf.cjs +1 -1
  4. package/lib/cjs/NavigationStack.js +138 -49
  5. package/lib/cjs/data-storage/DataStorage.js +7 -6
  6. package/lib/cjs/environment/InMemoryEnvironment.js +6 -0
  7. package/lib/cjs/{session/ServerSideRenderSession.js → environment/ServerSideRenderEnvironment.js} +5 -6
  8. package/lib/cjs/environment/WebBrowserEnvironment.js +6 -0
  9. package/lib/cjs/environment/log/InMemoryLog.js +23 -0
  10. package/lib/cjs/environment/log/WebBrowserLog.js +22 -0
  11. package/lib/cjs/{session → environment}/navigation/InMemoryNavigation.js +16 -5
  12. package/lib/cjs/{session → environment}/navigation/ServerSideNavigation.js +16 -7
  13. package/lib/cjs/{session → environment}/navigation/WebBrowserNavigation.js +48 -8
  14. package/lib/cjs/{session/navigation/error/ServerSideNavigationError.js → environment/navigation/error/ServerSideRedirectError.js} +2 -2
  15. package/lib/cjs/environment/scroll-position/WebBrowserScrollPosition.js +15 -0
  16. package/lib/cjs/getLocationBaseFromLocation.js +14 -0
  17. package/lib/cjs/getLocationUrl.js +3 -5
  18. package/lib/cjs/index.js +10 -16
  19. package/lib/cjs/navigationBlockers.js +34 -32
  20. package/lib/cjs/navigationBlockersEvaluation.js +150 -0
  21. package/lib/cjs/parseInputLocation.js +10 -3
  22. package/lib/cjs/parseQueryFromSearch.js +3 -6
  23. package/lib/cjs/parseQueryString.js +77 -0
  24. package/lib/cjs/scroll-position/ScrollPositionAutoSaver.js +7 -6
  25. package/lib/cjs/scroll-position/ScrollPositionRestoration.js +31 -27
  26. package/lib/cjs/scroll-position/ScrollPositionSaver.js +6 -4
  27. package/lib/cjs/session/Session.js +61 -26
  28. package/lib/cjs/session/subscription/Subscription.js +36 -18
  29. package/lib/cjs/stringifyQuery.js +66 -0
  30. package/lib/cjs/stringifyQueryAsSearch.js +14 -0
  31. package/lib/esm/NavigationStack.js +138 -49
  32. package/lib/esm/data-storage/DataStorage.js +7 -6
  33. package/lib/esm/environment/InMemoryEnvironment.js +6 -0
  34. package/lib/esm/environment/ServerSideRenderEnvironment.js +10 -0
  35. package/lib/esm/environment/WebBrowserEnvironment.js +6 -0
  36. package/lib/esm/environment/log/InMemoryLog.js +17 -0
  37. package/lib/esm/environment/log/WebBrowserLog.js +16 -0
  38. package/lib/esm/{session → environment}/navigation/InMemoryNavigation.js +16 -5
  39. package/lib/esm/{session → environment}/navigation/ServerSideNavigation.js +16 -7
  40. package/lib/esm/{session → environment}/navigation/WebBrowserNavigation.js +48 -8
  41. package/lib/esm/{session/navigation/error/ServerSideNavigationError.js → environment/navigation/error/ServerSideRedirectError.js} +1 -1
  42. package/lib/esm/environment/scroll-position/WebBrowserScrollPosition.js +15 -0
  43. package/lib/esm/getLocationBaseFromLocation.js +9 -0
  44. package/lib/esm/getLocationUrl.js +2 -5
  45. package/lib/esm/index.js +5 -8
  46. package/lib/esm/navigationBlockers.js +34 -32
  47. package/lib/esm/navigationBlockersEvaluation.js +145 -0
  48. package/lib/esm/parseInputLocation.js +9 -3
  49. package/lib/esm/parseQueryFromSearch.js +2 -6
  50. package/lib/esm/parseQueryString.js +72 -0
  51. package/lib/esm/scroll-position/ScrollPositionAutoSaver.js +7 -6
  52. package/lib/esm/scroll-position/ScrollPositionRestoration.js +31 -27
  53. package/lib/esm/scroll-position/ScrollPositionSaver.js +6 -4
  54. package/lib/esm/session/Session.js +61 -26
  55. package/lib/esm/session/subscription/Subscription.js +36 -18
  56. package/lib/esm/stringifyQuery.js +61 -0
  57. package/lib/esm/stringifyQueryAsSearch.js +8 -0
  58. package/lib/index.d.ts +180 -34
  59. package/package.json +4 -7
  60. package/src/NavigationStack.js +166 -56
  61. package/src/data-storage/DataStorage.js +9 -6
  62. package/src/environment/InMemoryEnvironment.js +6 -0
  63. package/src/environment/ServerSideRenderEnvironment.js +10 -0
  64. package/src/environment/WebBrowserEnvironment.js +6 -0
  65. package/src/environment/log/InMemoryLog.js +20 -0
  66. package/src/environment/log/WebBrowserLog.js +18 -0
  67. package/src/{session → environment}/navigation/InMemoryNavigation.js +16 -5
  68. package/src/{session → environment}/navigation/ServerSideNavigation.js +16 -7
  69. package/src/{session → environment}/navigation/WebBrowserNavigation.js +48 -8
  70. package/src/{session/navigation/error/ServerSideNavigationError.js → environment/navigation/error/ServerSideRedirectError.js} +1 -1
  71. package/src/environment/scroll-position/WebBrowserScrollPosition.js +15 -0
  72. package/src/getLocationBaseFromLocation.js +7 -0
  73. package/src/getLocationUrl.js +2 -5
  74. package/src/index.js +10 -13
  75. package/src/navigationBlockers.js +55 -34
  76. package/src/navigationBlockersEvaluation.js +161 -0
  77. package/src/parseInputLocation.js +10 -3
  78. package/src/parseQueryFromSearch.js +2 -6
  79. package/src/parseQueryString.js +81 -0
  80. package/src/scroll-position/ScrollPositionAutoSaver.js +10 -6
  81. package/src/scroll-position/ScrollPositionRestoration.js +36 -30
  82. package/src/scroll-position/ScrollPositionSaver.js +6 -4
  83. package/src/scroll-position/index.js +1 -1
  84. package/src/session/Session.js +68 -24
  85. package/src/session/subscription/Subscription.js +36 -11
  86. package/src/stringifyQuery.js +71 -0
  87. package/src/stringifyQueryAsSearch.js +9 -0
  88. package/test/NavigationStack.addBasePath.test.js +50 -0
  89. package/test/{redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.test.js → NavigationStack.blockNonProgrammaticNavigationIfRequired.test.js} +51 -63
  90. package/test/{redux/middleware/createProgrammaticNavigationBlockerMiddleware.test.js → NavigationStack.blockProgrammaticNavigationIfRequired.test.js} +98 -78
  91. package/test/NavigationStack.general.test.js +68 -0
  92. package/test/NavigationStack.parseInputLocation.test.js +52 -0
  93. package/test/NavigationStack.removeBasePath.test.js +69 -0
  94. package/test/NavigationStack.test.js +97 -29
  95. package/test/data-storage/LocationDataStorage.test.js +3 -2
  96. package/test/index.js +7 -31
  97. package/test/index.test.js +4 -5
  98. package/test/parseQueryFromSearch.test.js +19 -0
  99. package/test/parseQueryString.test.js +18 -0
  100. package/test/scroll-position/ScrollPositionRestoration.test.js +34 -13
  101. package/test/scroll-position/createApp.js +8 -8
  102. package/test/scroll-position/withScrollableContainerAtIndexPageWithDisabledAutomaticScrollPositionRestoration.js +4 -4
  103. package/test/session/{InMemorySession.test.js → Session.InMemoryEnvironment.test.js} +10 -9
  104. package/test/session/{ServerSession.test.js → Session.ServerSideRenderEnvironment.test.js} +5 -4
  105. package/test/session/{WebBrowserSession.test.js → Session.WebBrowserEnvironment.test.js} +63 -13
  106. package/test/shouldWarn.js +44 -0
  107. package/test/stringifyQuery.test.js +65 -0
  108. package/types/index.d.ts +180 -34
  109. package/types/tsconfig.json +0 -1
  110. package/data-storage/package.json +0 -7
  111. package/lib/cjs/createSearchFromQuery.js +0 -13
  112. package/lib/cjs/debug.js +0 -12
  113. package/lib/cjs/redux/ActionTypes.js +0 -14
  114. package/lib/cjs/redux/ActionTypesInternal.js +0 -8
  115. package/lib/cjs/redux/Actions.js +0 -28
  116. package/lib/cjs/redux/createMiddlewares.js +0 -60
  117. package/lib/cjs/redux/index.js +0 -13
  118. package/lib/cjs/redux/internalLocationReducer.js +0 -14
  119. package/lib/cjs/redux/locationReducer.js +0 -13
  120. package/lib/cjs/redux/middleware/createAddInputLocationBasePathMiddleware.js +0 -32
  121. package/lib/cjs/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +0 -113
  122. package/lib/cjs/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +0 -94
  123. package/lib/cjs/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +0 -30
  124. package/lib/cjs/redux/middleware/createUpdateInternalLocationMiddleware.js +0 -73
  125. package/lib/cjs/redux/middleware/navigationOperationMiddleware.js +0 -40
  126. package/lib/cjs/redux/middleware/parseInputLocationMiddleware.js +0 -29
  127. package/lib/cjs/redux/middleware/updateLocationMiddleware.js +0 -34
  128. package/lib/cjs/session/InMemorySession.js +0 -22
  129. package/lib/cjs/session/WebBrowserSession.js +0 -20
  130. package/lib/data-storage/index.d.ts +0 -35
  131. package/lib/esm/createSearchFromQuery.js +0 -8
  132. package/lib/esm/debug.js +0 -7
  133. package/lib/esm/redux/ActionTypes.js +0 -9
  134. package/lib/esm/redux/ActionTypesInternal.js +0 -3
  135. package/lib/esm/redux/Actions.js +0 -22
  136. package/lib/esm/redux/createMiddlewares.js +0 -54
  137. package/lib/esm/redux/index.js +0 -4
  138. package/lib/esm/redux/internalLocationReducer.js +0 -8
  139. package/lib/esm/redux/locationReducer.js +0 -7
  140. package/lib/esm/redux/middleware/createAddInputLocationBasePathMiddleware.js +0 -27
  141. package/lib/esm/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +0 -108
  142. package/lib/esm/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +0 -88
  143. package/lib/esm/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +0 -25
  144. package/lib/esm/redux/middleware/createUpdateInternalLocationMiddleware.js +0 -68
  145. package/lib/esm/redux/middleware/navigationOperationMiddleware.js +0 -35
  146. package/lib/esm/redux/middleware/parseInputLocationMiddleware.js +0 -24
  147. package/lib/esm/redux/middleware/updateLocationMiddleware.js +0 -28
  148. package/lib/esm/session/InMemorySession.js +0 -15
  149. package/lib/esm/session/ServerSideRenderSession.js +0 -11
  150. package/lib/esm/session/WebBrowserSession.js +0 -13
  151. package/lib/redux/index.d.ts +0 -90
  152. package/lib/scroll-position/index.d.ts +0 -107
  153. package/redux/package.json +0 -7
  154. package/scroll-position/package.json +0 -7
  155. package/src/createSearchFromQuery.js +0 -9
  156. package/src/debug.js +0 -8
  157. package/src/redux/ActionTypes.js +0 -9
  158. package/src/redux/ActionTypesInternal.js +0 -3
  159. package/src/redux/Actions.js +0 -27
  160. package/src/redux/createMiddlewares.js +0 -65
  161. package/src/redux/index.js +0 -4
  162. package/src/redux/internalLocationReducer.js +0 -9
  163. package/src/redux/locationReducer.js +0 -8
  164. package/src/redux/middleware/createAddInputLocationBasePathMiddleware.js +0 -27
  165. package/src/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +0 -119
  166. package/src/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +0 -94
  167. package/src/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +0 -26
  168. package/src/redux/middleware/createUpdateInternalLocationMiddleware.js +0 -72
  169. package/src/redux/middleware/navigationOperationMiddleware.js +0 -34
  170. package/src/redux/middleware/parseInputLocationMiddleware.js +0 -23
  171. package/src/redux/middleware/updateLocationMiddleware.js +0 -28
  172. package/src/session/InMemorySession.js +0 -13
  173. package/src/session/ServerSideRenderSession.js +0 -9
  174. package/src/session/WebBrowserSession.js +0 -13
  175. package/test/middlewareTestUtil.js +0 -31
  176. package/test/redux/Action.test.js +0 -73
  177. package/test/redux/ActionTypes.test.js +0 -13
  178. package/test/redux/createMiddlewares.test.js +0 -96
  179. package/test/redux/index.test.js +0 -10
  180. package/test/redux/locationReducer.test.js +0 -39
  181. package/test/redux/middleware/createAddInputLocationBasePathMiddleware.test.js +0 -40
  182. package/test/redux/middleware/createRemoveOutputLocationBasePathMiddleware.test.js +0 -51
  183. package/test/redux/middleware/navigationOperationMiddleware.test.js +0 -78
  184. package/test/redux/middleware/parseInputLocationMiddleware.test.js +0 -62
  185. package/test/testUtil.js +0 -3
  186. package/types/data-storage/index.d.ts +0 -35
  187. package/types/redux/index.d.ts +0 -90
  188. package/types/scroll-position/index.d.ts +0 -107
  189. /package/lib/cjs/{session → environment}/lifecycle/InMemorySessionLifecycle.js +0 -0
  190. /package/lib/cjs/{session → environment}/lifecycle/WebBrowserSessionLifecycle.js +0 -0
  191. /package/lib/cjs/{session → environment}/lifecycle/page-lifecycle/PageLifecycle.js +0 -0
  192. /package/lib/cjs/{session → environment}/lifecycle/page-lifecycle/PageLifecycleInstance.js +0 -0
  193. /package/lib/cjs/{session → environment}/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +0 -0
  194. /package/lib/cjs/{session → environment}/navigation/error/NavigationOutOfBoundsError.js +0 -0
  195. /package/lib/cjs/{session → environment}/navigation/operation/operations.js +0 -0
  196. /package/lib/esm/{session → environment}/lifecycle/InMemorySessionLifecycle.js +0 -0
  197. /package/lib/esm/{session → environment}/lifecycle/WebBrowserSessionLifecycle.js +0 -0
  198. /package/lib/esm/{session → environment}/lifecycle/page-lifecycle/PageLifecycle.js +0 -0
  199. /package/lib/esm/{session → environment}/lifecycle/page-lifecycle/PageLifecycleInstance.js +0 -0
  200. /package/lib/esm/{session → environment}/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +0 -0
  201. /package/lib/esm/{session → environment}/navigation/error/NavigationOutOfBoundsError.js +0 -0
  202. /package/lib/esm/{session → environment}/navigation/operation/operations.js +0 -0
  203. /package/src/{session → environment}/lifecycle/InMemorySessionLifecycle.js +0 -0
  204. /package/src/{session → environment}/lifecycle/WebBrowserSessionLifecycle.js +0 -0
  205. /package/src/{session → environment}/lifecycle/page-lifecycle/PageLifecycle.js +0 -0
  206. /package/src/{session → environment}/lifecycle/page-lifecycle/PageLifecycleInstance.js +0 -0
  207. /package/src/{session → environment}/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +0 -0
  208. /package/src/{session → environment}/navigation/error/NavigationOutOfBoundsError.js +0 -0
  209. /package/src/{session → environment}/navigation/operation/operations.js +0 -0
  210. /package/test/{parseInputLocationMiddleware.test.js → parseInputLocation.test.js} +0 -0
@@ -2,21 +2,23 @@
2
2
 
3
3
  import ScrollPositionAutoSaver from './ScrollPositionAutoSaver';
4
4
  import { PAGE_SCROLLABLE_CONTAINER_KEY } from './constants';
5
- import debug from '../debug';
6
5
  export default class ScrollPositionSaver {
7
6
  constructor({
7
+ log,
8
8
  scrollPosition,
9
9
  getLocation,
10
10
  saveScrollPositionForLocation,
11
11
  getScrollableContainers,
12
12
  shouldSaveScrollPosition
13
13
  }) {
14
+ this._log = log;
14
15
  this._scrollPosition = scrollPosition;
15
16
  this._getLocation = getLocation;
16
17
  this._saveScrollPositionForLocation = saveScrollPositionForLocation;
17
18
  this._getScrollableContainers = getScrollableContainers;
18
19
  this._shouldSaveScrollPosition = shouldSaveScrollPosition;
19
20
  this._scrollPositionAutoSaver = new ScrollPositionAutoSaver({
21
+ log: this._log,
20
22
  scrollPosition: this._scrollPosition,
21
23
  scrollPositionSaver: this,
22
24
  getScrollableContainers,
@@ -37,7 +39,7 @@ export default class ScrollPositionSaver {
37
39
  if (!this._shouldSaveScrollPosition()) {
38
40
  return;
39
41
  }
40
- debug('save scroll position', this._getLocation().pathname);
42
+ this._log.debug('save scroll position', this._getLocation().pathname);
41
43
 
42
44
  // Get scrollable containers.
43
45
  const scrollableContainers = this._getScrollableContainers();
@@ -52,7 +54,7 @@ export default class ScrollPositionSaver {
52
54
  }
53
55
  }
54
56
  savePageScrollPosition() {
55
- debug('save scroll position', this._getLocation().pathname, PAGE_SCROLLABLE_CONTAINER_KEY, this._scrollPosition.getPageScrollPosition());
57
+ this._log.debug('save scroll position', this._getLocation().pathname, PAGE_SCROLLABLE_CONTAINER_KEY, this._scrollPosition.getPageScrollPosition());
56
58
 
57
59
  // * If this is not a scheduled "auto-save" of scroll position
58
60
  // and there already exists any scheduled "auto-save" of scroll position,
@@ -65,7 +67,7 @@ export default class ScrollPositionSaver {
65
67
  this._saveScrollPositionForLocation(this._getLocation(), undefined, this._scrollPosition.getPageScrollPosition());
66
68
  }
67
69
  saveScrollableContainerScrollPosition(scrollableContainerKey, scrollableContainer) {
68
- debug('save scroll position', this._getLocation().pathname, scrollableContainerKey, this._scrollPosition.getScrollableContainerScrollPosition(scrollableContainer));
70
+ this._log.debug('save scroll position', this._getLocation().pathname, scrollableContainerKey, this._scrollPosition.getScrollableContainerScrollPosition(scrollableContainer));
69
71
 
70
72
  // * If this is not a scheduled "auto-save" of scroll position
71
73
  // and there already exists any scheduled "auto-save" of scroll position,
@@ -1,16 +1,13 @@
1
- import debug from '../debug';
2
1
  import parseInputLocation from '../parseInputLocation';
3
2
  import createSessionKey from './key/createSessionKey';
4
- import NavigationOutOfBoundsError from './navigation/error/NavigationOutOfBoundsError';
5
- import NavigationOperations from './navigation/operation/operations';
6
3
  import Subscription from './subscription/Subscription';
4
+ import NavigationOutOfBoundsError from '../environment/navigation/error/NavigationOutOfBoundsError';
5
+ import NavigationOperations from '../environment/navigation/operation/operations';
7
6
  const INITIAL_KEY_INDEX = -1;
8
7
  const INITIAL_INDEX = -1;
9
8
  const INIT_LOCATION_DELTA = 0;
10
9
  export default class Session {
11
- constructor({
12
- navigation
13
- }) {
10
+ constructor(EnvironmentClass) {
14
11
  // This function is used by navigation.
15
12
  this._getCurrentLocationIndex = () => {
16
13
  return this._currentLocationIndex;
@@ -20,6 +17,9 @@ export default class Session {
20
17
  // under the hood, and `window.sessionStorage` is shared between different sessions.
21
18
  this.key = createSessionKey();
22
19
 
20
+ // Create an environment instance.
21
+ this.environment = new EnvironmentClass();
22
+
23
23
  // `this._locationKeyIndex` is incremented every time the current location changes.
24
24
  this._locationKeyIndex = INITIAL_KEY_INDEX;
25
25
 
@@ -31,18 +31,34 @@ export default class Session {
31
31
  // In other words, this is the last location index that it can `.shift()` to.
32
32
  this._terminalLocationIndex = this._currentLocationIndex;
33
33
 
34
- // Create `navigation`.
35
- this._navigation = navigation;
34
+ // Allows subscribing to location updates.
35
+ this._subscription = new Subscription();
36
36
 
37
- // Manages subscriptions.
38
- this._subscription = new Subscription({
39
- activateSubscription: listener => {
40
- return this._navigation.subscribe(listener);
41
- }
37
+ // Subscribing to location changes means subscribing to both "synchronous"
38
+ // and "asynchronous" location changes. "synchronous" location changes
39
+ // happen immediately when the code triggers them."asynchronous" location changes
40
+ // either happen after an arbitrary delay or are even triggered from outside the code.
41
+ //
42
+ // Subscribing to "asynchronous" location changes is not necessary when
43
+ // there're no actual subscribers, in order to not unnecessarily "waste" any resources.
44
+ // Of course, this statement is rather far-fetched and in reality no one would ever tell any difference.
45
+ // Still, I felt like randomly introducing this seemingly unnecessary minor optimization.
46
+ //
47
+ // So it only subscribes to "asynchronous" location changes if there's at least one active subscriber.
48
+ // And in case all subscribers get unsubscribed, it will unsubscribe from "asynchronous" location changes too.
49
+ // One might think of it as some form of "mental masturbation", but what can I do — I already wrote the code.
50
+ //
51
+ this._subscription.onFirstSubscriber(() => {
52
+ return this.environment.navigation.subscribeToAsyncrhonousLocationUpdates(location => {
53
+ // Notify all subscribers about this "asynchronous" location change.
54
+ this._subscription.notifySubscribers(location);
55
+ });
42
56
  });
43
57
 
44
- // Update current location index when a location change was not initiated
45
- // by this session but rather by the user clicking "Back" or "Forward" button.
58
+ // This subscription is triggered in two cases:
59
+ // * Set initial current location index at initial page load.
60
+ // * Update current location index whenever a location change is not initiated
61
+ // by this session but rather by the user clicking "Back" or "Forward" button.
46
62
  this._unsubscribe = this.subscribe(location => {
47
63
  // Update `this._currentLocationIndex` when the location change was not initiated
48
64
  // by this session but rather by the user clicking "Back" or "Forward" button.
@@ -52,16 +68,20 @@ export default class Session {
52
68
  // but if it was possible, this call would be required. It would also be required
53
69
  // by `navigation` to call `session.getNextKey()` function to increment `locationKeyIndex`.
54
70
  this._updateTerminalLocationIndex(location);
55
- debug('current location', location.pathname, 'index', this._currentLocationIndex);
71
+ this.environment.log.debug('current location', location.pathname, 'index', this._currentLocationIndex);
56
72
  });
57
73
  }
58
74
 
59
75
  // Subscribes to changes in location.
76
+ // The first subscriber is always the `Session` itself:
77
+ // its listener keeps the current location index up-to-date.
78
+ // Any additional application-specific listeners could be added, if required.
79
+ // Applications should prefer adding any such listeners by calling `NavigationStack.subscribe()`
80
+ // method instead of calling this method directly, in order to "normalize" the `location` argument.
60
81
  subscribe(listener) {
61
82
  return this._subscription.subscribe(location => {
62
83
  if (!this._isStarted() && location.operation !== NavigationOperations.INIT) {
63
- // eslint-disable-next-line no-console
64
- console.error('Unexpected location change', location);
84
+ this.environment.log.error('Unexpected location change', location);
65
85
  throw new Error('Not started');
66
86
  } else {
67
87
  // Call the listener.
@@ -69,6 +89,18 @@ export default class Session {
69
89
  }
70
90
  });
71
91
  }
92
+
93
+ // Starts a navigation session.
94
+ //
95
+ // When run in a web browser, it could not only "start" a new session
96
+ // but also "resume" a previously-started session. That could happen
97
+ // when the user refreshes a page in a web browser which still retains
98
+ // the previous session's data but at the same time restarts the javascript code
99
+ // from scratch.
100
+ //
101
+ // So this `start()` method handles both cases: when there's previous session's data
102
+ // that should be restored and when there's no previous session's data.
103
+ //
72
104
  start(initialLocation) {
73
105
  if (this._stopped) {
74
106
  throw new Error('Can not be restarted');
@@ -81,7 +113,7 @@ export default class Session {
81
113
  // the initial location by the time javascript code starts execution.
82
114
  //
83
115
  if (!initialLocation) {
84
- initialLocation = this._navigation.getInitialLocation();
116
+ initialLocation = this.environment.navigation.getInitialLocation();
85
117
  if (initialLocation) {
86
118
  initialLocation = parseInputLocation(initialLocation);
87
119
  }
@@ -92,18 +124,19 @@ export default class Session {
92
124
  if (this._currentLocationIndex !== INITIAL_INDEX) {
93
125
  throw new Error('Already started');
94
126
  }
95
- debug('▶ start session', initialLocation.pathname);
127
+ this.environment.log.debug('▶ start session', initialLocation.pathname);
96
128
  this._started = true;
97
129
  const key = this._getNextLocationKey();
98
130
  const index = INITIAL_INDEX + 1;
99
131
  const delta = INIT_LOCATION_DELTA;
100
- const locationResult = this._navigation.init(initialLocation, {
132
+ const locationResult = this.environment.navigation.init(initialLocation, {
101
133
  operation: NavigationOperations.INIT,
102
134
  key,
103
135
  index,
104
136
  delta
105
137
  });
106
138
  if (locationResult) {
139
+ // Notify all subscribers about this "synchronous" location change.
107
140
  this._subscription.notifySubscribers(locationResult);
108
141
  }
109
142
  }
@@ -111,7 +144,7 @@ export default class Session {
111
144
  if (this._stopped) {
112
145
  throw Error('Already stopped');
113
146
  }
114
- debug('⏹ stop session');
147
+ this.environment.log.debug('⏹ stop session');
115
148
 
116
149
  // Once stopped, it won't be able to be restarted.
117
150
  this._stopped = true;
@@ -137,16 +170,17 @@ export default class Session {
137
170
  });
138
171
  const key = this._getNextLocationKey();
139
172
  const index = this._currentLocationIndex + delta;
140
- debug(operation === NavigationOperations.PUSH ? '↓' : '⇅', operation, location.pathname, 'index', index);
173
+ this.environment.log.debug(operation === NavigationOperations.PUSH ? '↓' : '⇅', operation, location.pathname, 'index', index);
141
174
 
142
175
  // Navigate to the location.
143
- const locationResult = this._navigation.navigate(location, {
176
+ const locationResult = this.environment.navigation.navigate(location, {
144
177
  operation,
145
178
  key,
146
179
  index,
147
180
  delta
148
181
  });
149
182
  if (locationResult) {
183
+ // Notify all subscribers about this "synchronous" location change.
150
184
  this._subscription.notifySubscribers(locationResult);
151
185
  }
152
186
  }
@@ -160,7 +194,7 @@ export default class Session {
160
194
  return;
161
195
  }
162
196
  const index = this._currentLocationIndex + delta;
163
- debug(delta > 0 ? '→' : '←', 'shift', delta, 'index', index);
197
+ this.environment.log.debug(delta > 0 ? '→' : '←', 'shift', delta, 'index', index);
164
198
 
165
199
  // Validate that the new `index` is not out of bounds.
166
200
  if (index < 0 || index > this._terminalLocationIndex) {
@@ -168,12 +202,13 @@ export default class Session {
168
202
  }
169
203
 
170
204
  // Navigate to the location.
171
- const locationResult = this._navigation.shift({
205
+ const locationResult = this.environment.navigation.shift({
172
206
  operation: NavigationOperations.SHIFT,
173
207
  index,
174
208
  delta
175
209
  });
176
210
  if (locationResult) {
211
+ // Notify all subscribers about this "synchronous" location change.
177
212
  this._subscription.notifySubscribers(locationResult);
178
213
  }
179
214
  }
@@ -1,20 +1,32 @@
1
1
  export default class Subscription {
2
- constructor({
3
- activateSubscription
4
- } = {}) {
5
- this.notifySubscribers = argument => {
6
- // `._latest` is only used in tests.
7
- this._latest = argument;
8
- for (const {
9
- listener
10
- } of this._listeners) {
11
- listener(argument);
12
- }
13
- };
14
- this._activateSubscription = activateSubscription;
15
-
2
+ constructor() {
16
3
  // This property is accessed in tests.
17
4
  this._listeners = [];
5
+
6
+ // These listeners will be called when the subscription enters "active" or "inactive" state.
7
+ // A subscription enters "active" state when it has at least one listener rather than zero.
8
+ // A subscription enters "inactive" state when it has no more listeners.
9
+ this._subscriptionActiveStateListeners = [];
10
+ this._subscriptionInactiveStateListeners = [];
11
+ }
12
+
13
+ // Adds a subscription active state listener.
14
+ // Returns a function that removes the subscription active state listener.
15
+ onFirstSubscriber(activeStateListener) {
16
+ this._subscriptionActiveStateListeners.push(activeStateListener);
17
+ // Return a function that removes the subscription active state listener.
18
+ return () => {
19
+ this._subscriptionActiveStateListeners = this._subscriptionActiveStateListeners.filter(_ => _ !== activeStateListener);
20
+ };
21
+ }
22
+ notifySubscribers(argument) {
23
+ // `._latest` is only used in tests.
24
+ this._latest = argument;
25
+ for (const {
26
+ listener
27
+ } of this._listeners) {
28
+ listener(argument);
29
+ }
18
30
  }
19
31
  subscribe(listener) {
20
32
  // If subscriptions are stopped, i.e. no new subscriptions are to be added,
@@ -34,7 +46,9 @@ export default class Subscription {
34
46
 
35
47
  // If it's the first listener, activate subscription.
36
48
  if (this._listeners.length === 0) {
37
- this._deactivateSubscription = this._activateSubscription(this.notifySubscribers);
49
+ // Run all subscription active state listeners.
50
+ // The functions returned from those will become subscription inactive state listeners.
51
+ this._subscriptionInactiveStateListeners = this._subscriptionActiveStateListeners.map(activeStateListener => activeStateListener());
38
52
  }
39
53
 
40
54
  // Add the `listener` to the list.
@@ -66,10 +80,14 @@ export default class Subscription {
66
80
  // Remove the `listener` from the list.
67
81
  this._listeners = this._listeners.filter(_ => _ !== listenerEntry);
68
82
 
69
- // If it was the last listener, deactivate subscription.
83
+ // If it was the last listener.
70
84
  if (this._listeners.length === 0) {
71
- this._deactivateSubscription();
72
- this._deactivateSubscription = undefined;
85
+ // Run any subscription inactive state listeners,
86
+ // after which clear the list of such listeners.
87
+ for (const inactiveStateListener of this._subscriptionInactiveStateListeners) {
88
+ inactiveStateListener();
89
+ }
90
+ this._subscriptionInactiveStateListeners = [];
73
91
  }
74
92
  }
75
93
  }
@@ -0,0 +1,61 @@
1
+ // "The more recent RFC3986 reserves !, ', (, ), and *,
2
+ // even though these characters have no formalized URI delimiting uses.
3
+ //
4
+ // https://datatracker.ietf.org/doc/html/rfc3986
5
+ //
6
+ // The following function encodes a string for RFC3986-compliant URL component format.
7
+ // It also encodes [ and ], which are part of the IPv6 URI syntax.
8
+ //
9
+ // An RFC3986-compliant encodeURI implementation should not escape them,
10
+ // which is demonstrated in the encodeURI() example.
11
+ //
12
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#encoding_for_rfc3986
13
+ //
14
+ // Can throw a `URIError` if the `string` contains a "lone surrogate".
15
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String#utf-16_characters_unicode_code_points_and_grapheme_clusters
16
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/URIError
17
+ // Example: "URIError: malformed URI sequence"
18
+ //
19
+ function encode(string) {
20
+ return encodeURIComponent(string).replace(/[!'()*]/g, character => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
21
+ }
22
+ export default function stringifyQuery(query) {
23
+ let queryString = '';
24
+ if (!query) {
25
+ return queryString;
26
+ }
27
+ for (const key of Object.keys(query)) {
28
+ let value = query[key];
29
+ if (Array.isArray(value)) {
30
+ throw new Error('Array values are not supported');
31
+ }
32
+
33
+ // Ignore `value: undefined`.
34
+ if (value === undefined) {
35
+ continue;
36
+ }
37
+
38
+ // Stringify `value`.
39
+ if (value === null) {
40
+ value = '';
41
+ } else {
42
+ value = String(value);
43
+ }
44
+
45
+ // Can throw a `URIError` if the `string` contains a "lone surrogate".
46
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String#utf-16_characters_unicode_code_points_and_grapheme_clusters
47
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/URIError
48
+ // Example: "URIError: malformed URI sequence"
49
+ try {
50
+ const keyValuePair = `${encode(key)}${value ? '=' : ''}${encode(value)}`;
51
+ if (queryString.length > 1) {
52
+ queryString += '&';
53
+ }
54
+ queryString += keyValuePair;
55
+ } catch (error) {
56
+ // Simply ignore an invalid query parameter.
57
+ continue;
58
+ }
59
+ }
60
+ return queryString;
61
+ }
@@ -0,0 +1,8 @@
1
+ import stringifyQuery from './stringifyQuery';
2
+ export default function stringifyQueryAsSearch(query) {
3
+ const queryString = stringifyQuery(query);
4
+ if (queryString) {
5
+ return `?${queryString}`;
6
+ }
7
+ return '';
8
+ }