navigation-stack 0.1.3 → 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.
- package/.github/workflows/main.yml +39 -39
- package/README.md +128 -27
- package/lib/cjs/{LocationStateStorage.js → LocationDataStorage.js} +5 -5
- package/lib/cjs/addBeforeLocationChangeListener.js +7 -0
- package/lib/cjs/beforeLocationChangeListeners.js +51 -0
- package/lib/cjs/createMiddlewares.js +21 -17
- package/lib/cjs/index.js +9 -9
- package/lib/cjs/middleware/createBasePathMiddleware.js +2 -2
- package/lib/cjs/middleware/createBeforeLocationChangeListenerMiddleware.js +39 -0
- package/lib/cjs/middleware/{createEnvironmentMiddleware.js → createLocationMiddleware.js} +12 -14
- package/lib/cjs/middleware/createNavigationBlockerMiddleware.js +62 -29
- package/lib/cjs/middleware/createTransformLocationMiddleware.js +2 -2
- package/lib/cjs/navigationBlockers.js +55 -47
- package/lib/cjs/normalizeInputLocation.js +1 -0
- package/lib/cjs/parseLocationUrl.js +2 -0
- package/lib/cjs/parseQueryFromSearch.js +1 -1
- package/lib/cjs/session/BrowserSession.js +229 -0
- package/lib/cjs/session/MemorySession.js +223 -0
- package/lib/cjs/{environment/ServerEnvironment.js → session/ServerSession.js} +28 -16
- package/lib/esm/{LocationStateStorage.js → LocationDataStorage.js} +4 -4
- package/lib/esm/addBeforeLocationChangeListener.js +2 -0
- package/lib/esm/beforeLocationChangeListeners.js +44 -0
- package/lib/esm/createMiddlewares.js +21 -17
- package/lib/esm/index.js +4 -4
- package/lib/esm/middleware/createBasePathMiddleware.js +2 -2
- package/lib/esm/middleware/createBeforeLocationChangeListenerMiddleware.js +34 -0
- package/lib/esm/middleware/{createEnvironmentMiddleware.js → createLocationMiddleware.js} +11 -13
- package/lib/esm/middleware/createNavigationBlockerMiddleware.js +62 -29
- package/lib/esm/middleware/createTransformLocationMiddleware.js +2 -2
- package/lib/esm/navigationBlockers.js +55 -47
- package/lib/esm/normalizeInputLocation.js +1 -0
- package/lib/esm/parseLocationUrl.js +2 -0
- package/lib/esm/parseQueryFromSearch.js +1 -1
- package/lib/esm/session/BrowserSession.js +223 -0
- package/lib/esm/session/MemorySession.js +217 -0
- package/lib/esm/{environment/ServerEnvironment.js → session/ServerSession.js} +27 -15
- package/lib/index.d.ts +64 -59
- package/package.json +4 -4
- package/src/{LocationStateStorage.js → LocationDataStorage.js} +4 -4
- package/src/addBeforeLocationChangeListener.js +2 -0
- package/src/beforeLocationChangeListeners.js +54 -0
- package/src/createMiddlewares.js +21 -17
- package/src/index.js +4 -4
- package/src/middleware/createBasePathMiddleware.js +2 -2
- package/src/middleware/createBeforeLocationChangeListenerMiddleware.js +40 -0
- package/src/middleware/{createEnvironmentMiddleware.js → createLocationMiddleware.js} +12 -14
- package/src/middleware/createNavigationBlockerMiddleware.js +68 -28
- package/src/middleware/createTransformLocationMiddleware.js +2 -2
- package/src/navigationBlockers.js +68 -49
- package/src/normalizeInputLocation.js +1 -0
- package/src/parseLocationUrl.js +2 -0
- package/src/parseQueryFromSearch.js +1 -1
- package/src/session/BrowserSession.js +225 -0
- package/src/session/MemorySession.js +219 -0
- package/src/{environment/ServerEnvironment.js → session/ServerSession.js} +28 -15
- package/test/{LocationStateStorage.test.js → LocationDataStorage.test.js} +6 -6
- package/test/createMiddlewares.test.js +2 -2
- package/test/helpers.js +1 -1
- package/test/index.test.js +3 -3
- package/test/middleware/createBasePathMiddleware.test.js +7 -7
- package/test/middleware/createBeforeLocationChangeListenerMiddleware.test.js +141 -0
- package/test/middleware/createNavigationBlockerMiddleware.test.js +96 -97
- package/test/middleware/createTransformLocationMiddleware.test.js +1 -1
- package/test/normalizeInputLocation.test.js +3 -0
- package/test/parseLocationUrl.test.js +2 -0
- package/test/{environment/BrowserEnvironment.test.js → session/BrowserSession.test.js} +35 -18
- package/test/session/MemorySession.test.js +244 -0
- package/test/session/ServerSession.test.js +23 -0
- package/types/index.d.ts +64 -59
- package/lib/cjs/environment/BrowserEnvironment.js +0 -111
- package/lib/cjs/environment/MemoryEnvironment.js +0 -150
- package/lib/esm/environment/BrowserEnvironment.js +0 -104
- package/lib/esm/environment/MemoryEnvironment.js +0 -143
- package/src/environment/BrowserEnvironment.js +0 -109
- package/src/environment/MemoryEnvironment.js +0 -151
- package/test/environment/MemoryEnvironment.test.js +0 -218
- package/test/environment/ServerEnvironment.test.js +0 -23
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/* eslint-disable max-classes-per-file */
|
|
2
|
+
|
|
3
|
+
import getLocationUrl from '../getLocationUrl';
|
|
4
|
+
import parseQueryFromSearch from '../parseQueryFromSearch';
|
|
5
|
+
|
|
6
|
+
const INITIAL_KEY_INDEX = -1;
|
|
7
|
+
const INITIAL_INDEX = -1;
|
|
8
|
+
|
|
9
|
+
const INIT_LOCATION_DELTA = 0;
|
|
10
|
+
|
|
11
|
+
// A web browser has a notion of a "navigation history".
|
|
12
|
+
// A "navigation history" exists within a given web browser's tab.
|
|
13
|
+
// The user can click "Back" or "Forward" buttons in the web browser and it will automatically load
|
|
14
|
+
// "previous" or "next" page from scratch.
|
|
15
|
+
//
|
|
16
|
+
// Later, web browsers added a `window.history` object that the application can,
|
|
17
|
+
// but isn't required to, interact with. That `window.history` object allows the application
|
|
18
|
+
// to programmatically control the URL in the address bar of the web browser, as well as
|
|
19
|
+
// the "navigation history" by programmatically adding new entries to it or reading the current entry,
|
|
20
|
+
// and it also allows the application to override the default web browser's behavior
|
|
21
|
+
// when the user clicks "Back" or "Forward" buttons in the web browser.
|
|
22
|
+
//
|
|
23
|
+
// Specifically, the `window.history` object has a method called `.pushState()` which programmatically adds
|
|
24
|
+
// a new entry in the "navigation history" and updates the URL in the address bar and also
|
|
25
|
+
// tells the web browser that starting from the entry before this new entry in the "navigation history",
|
|
26
|
+
// the application would prefer to manually handle any "Back"/"Forward" transition when the user clicks
|
|
27
|
+
// those "Back" or "Forward" buttons in the web browser, and this behavior should persist for any future
|
|
28
|
+
// "navigation history" entries programmatically added by the application via `window.history.pushState()`,
|
|
29
|
+
// and will only stop if the user navigates from the page by the means of conventional navigation,
|
|
30
|
+
// that is by clicking a standard hyperlink, at which point the current page gets "destroyed".
|
|
31
|
+
//
|
|
32
|
+
// So for manually "pushed" entries of the "navigation history", the web browser won't load those pages
|
|
33
|
+
// from scratch after a user-initiated "Back" or "Forward" transition. In fact, it won't do anything and
|
|
34
|
+
// it will just step aside and let the application itself do those transitions. The web browser will only
|
|
35
|
+
// update the URL in the address bar and that's it.
|
|
36
|
+
//
|
|
37
|
+
// This whole thing allows the application to:
|
|
38
|
+
//
|
|
39
|
+
// * Load the "previous" or "next" page much faster than when using the default "from scratch" approach
|
|
40
|
+
// because it doesn't have to destroy the current page, then send a new HTTP request to the server,
|
|
41
|
+
// then parse the HTML response and initialize a new page, re-download all those images, etc.
|
|
42
|
+
//
|
|
43
|
+
// * Optionally render a snapshotted verison of the "previous" page thereby "restoring" the "previous" page
|
|
44
|
+
// rather than reloading it from scratch, i.e. the state of the "previous" page could be fully restored.
|
|
45
|
+
//
|
|
46
|
+
class BrowserNavigation {
|
|
47
|
+
constructor() {
|
|
48
|
+
// `this._keyPrefix` exists to avoid `this._keyIndex` collision after a page refresh.
|
|
49
|
+
// After a page refresh, `this._keyIndex` is reset to `0` while the previous navigation history
|
|
50
|
+
// still exists because web browser navigation history survives a page reload.
|
|
51
|
+
this._keyPrefix = Date.now().toString(36);
|
|
52
|
+
// `this._keyIndex` is incremented every time the current location changes.
|
|
53
|
+
this._keyIndex = INITIAL_KEY_INDEX;
|
|
54
|
+
|
|
55
|
+
// `this._index` is the index of the top element in the navigation stack.
|
|
56
|
+
// I.e. it's the index of the "current" location in the navigation stack.
|
|
57
|
+
this._index = INITIAL_INDEX;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
init() {
|
|
61
|
+
return this._createEntryFromCurrentLocation();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
_createEntryFromCurrentLocation() {
|
|
65
|
+
const { pathname, search, hash } = window.location;
|
|
66
|
+
|
|
67
|
+
const isSettingInitialLocation = this._index === INITIAL_INDEX;
|
|
68
|
+
|
|
69
|
+
const { key, index, delta, state } = isSettingInitialLocation
|
|
70
|
+
? this._createAdditionalPropertiesForNewLocation({
|
|
71
|
+
delta: 1,
|
|
72
|
+
state: undefined,
|
|
73
|
+
})
|
|
74
|
+
: this._restoreAdditionalPropertiesForCurrentLocation();
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
action: isSettingInitialLocation ? 'INIT' : 'POP',
|
|
78
|
+
pathname,
|
|
79
|
+
search,
|
|
80
|
+
query: parseQueryFromSearch(search),
|
|
81
|
+
hash,
|
|
82
|
+
key,
|
|
83
|
+
index,
|
|
84
|
+
delta: isSettingInitialLocation ? INIT_LOCATION_DELTA : delta,
|
|
85
|
+
state,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Subscribes to changes in location,
|
|
90
|
+
// excluding ones that happened as a result of calling `.navigate()`.
|
|
91
|
+
subscribe(listener) {
|
|
92
|
+
const onPopState = () => {
|
|
93
|
+
listener(this._createEntryFromCurrentLocation());
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
window.addEventListener('popstate', onPopState);
|
|
97
|
+
return () => {
|
|
98
|
+
window.removeEventListener('popstate', onPopState);
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
navigate(location) {
|
|
103
|
+
const { action, state } = location;
|
|
104
|
+
|
|
105
|
+
if (action !== 'PUSH' && action !== 'REPLACE') {
|
|
106
|
+
throw Error(`Unrecognized browser session action: ${action}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (this._index === INITIAL_INDEX) {
|
|
110
|
+
throw Error('Browser session must be initialized before navigation');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const delta = action === 'PUSH' ? 1 : 0;
|
|
114
|
+
|
|
115
|
+
const additionalProperties =
|
|
116
|
+
this._createAdditionalPropertiesForNewLocation({ delta, state });
|
|
117
|
+
|
|
118
|
+
this._storeAdditionalPropertiesForLocation(location, additionalProperties);
|
|
119
|
+
|
|
120
|
+
return { ...location, ...additionalProperties };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
shift(delta) {
|
|
124
|
+
window.history.go(delta);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
_createKeyForKeyIndex(keyIndex) {
|
|
128
|
+
return `${this._keyPrefix}.${keyIndex.toString(36)}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
_createAdditionalPropertiesForNewLocation({ delta, state }) {
|
|
132
|
+
this._keyIndex++;
|
|
133
|
+
this._index += delta;
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
key: this._createKeyForKeyIndex(this._keyIndex),
|
|
137
|
+
index: this._index,
|
|
138
|
+
delta,
|
|
139
|
+
state,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
_restoreAdditionalPropertiesForCurrentLocation() {
|
|
144
|
+
// Initial location doesn't have any `window.history.state` assigned to it
|
|
145
|
+
// because it wasn't navigated to via a `window.history.pushState()` method.
|
|
146
|
+
// Because of that, the additional properties for the initial location can't be read
|
|
147
|
+
// from `window.history.state` and have to be reconstructed manually.
|
|
148
|
+
const { key, index, state } =
|
|
149
|
+
window.history.state ||
|
|
150
|
+
this._getAdditionalPropertiesForInitialLocation();
|
|
151
|
+
const delta = index - this._index;
|
|
152
|
+
this._index = index;
|
|
153
|
+
return { key, index, delta, state };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
_storeAdditionalPropertiesForLocation(location, additionalProperties) {
|
|
157
|
+
const url = getLocationUrl(location);
|
|
158
|
+
// `delta` property is not stored in `window.history.state`
|
|
159
|
+
// because it is supposed to be recalculated every time when reading from `window.history.state`.
|
|
160
|
+
const { delta, ...restProperties } = additionalProperties;
|
|
161
|
+
if (delta === 1) {
|
|
162
|
+
window.history.pushState(restProperties, null, url);
|
|
163
|
+
} else if (delta === 0) {
|
|
164
|
+
window.history.replaceState(restProperties, null, url);
|
|
165
|
+
} else {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`Unexpected \`delta\` when storing additional properties for location: ${delta}`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Initial location doesn't have any `window.history.state` assigned to it
|
|
173
|
+
// because it wasn't navigated to via a `window.history.pushState()` method.
|
|
174
|
+
// Because of that, the additional properties for the initial location can't be read
|
|
175
|
+
// from `window.history.state` and have to be reconstructed manually.
|
|
176
|
+
_getAdditionalPropertiesForInitialLocation() {
|
|
177
|
+
return {
|
|
178
|
+
key: this._createKeyForKeyIndex(INITIAL_KEY_INDEX + 1),
|
|
179
|
+
index: INITIAL_INDEX + 1,
|
|
180
|
+
delta: INIT_LOCATION_DELTA,
|
|
181
|
+
state: undefined,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
class BrowserDataStorage {
|
|
187
|
+
// Returns either a `string` value or `null` if the key doesn't exist.
|
|
188
|
+
get(key) {
|
|
189
|
+
// `sessionStorage` persists across page reloads, and so does web browser navigation history.
|
|
190
|
+
return window.sessionStorage.getItem(key);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
remove(key) {
|
|
194
|
+
// `sessionStorage` persists across page reloads, and so does web browser navigation history.
|
|
195
|
+
window.sessionStorage.removeItem(key);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
set(key, value) {
|
|
199
|
+
// `sessionStorage` persists across page reloads, and so does web browser navigation history.
|
|
200
|
+
window.sessionStorage.setItem(key, value);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export default class BrowserSession {
|
|
205
|
+
constructor() {
|
|
206
|
+
this.navigation = new BrowserNavigation();
|
|
207
|
+
this.dataStorage = new BrowserDataStorage();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
addBeforeDestroyListener(onBeforeDestroy) {
|
|
211
|
+
const onBeforeUnload = (event) => {
|
|
212
|
+
if (onBeforeDestroy()) {
|
|
213
|
+
// Calling `event.preventDefault()` will cause a web browser
|
|
214
|
+
// to show a generic "Ok"/"Cancel" modal with some generic text:
|
|
215
|
+
// "Are you sure to leave the current page?".
|
|
216
|
+
event.preventDefault();
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
window.addEventListener('beforeunload', onBeforeUnload);
|
|
221
|
+
return () => {
|
|
222
|
+
window.removeEventListener('beforeunload', onBeforeUnload);
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/* eslint-disable max-classes-per-file */
|
|
2
|
+
|
|
3
|
+
import normalizeInputLocation from '../normalizeInputLocation';
|
|
4
|
+
|
|
5
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
6
|
+
function _loadState(load, isValidLoadedData) {
|
|
7
|
+
try {
|
|
8
|
+
const data = JSON.parse(load());
|
|
9
|
+
|
|
10
|
+
// Check that the stack and index at least seem reasonable before using
|
|
11
|
+
// them as state. This isn't foolproof, but it might prevent mistakes.
|
|
12
|
+
// Also perform a basic validation of `state`.
|
|
13
|
+
if (isValidLoadedData(data)) {
|
|
14
|
+
return data;
|
|
15
|
+
}
|
|
16
|
+
} catch (error) {} // eslint-disable-line no-empty
|
|
17
|
+
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
22
|
+
function _saveState(save, data) {
|
|
23
|
+
try {
|
|
24
|
+
save(JSON.stringify(data));
|
|
25
|
+
} catch (error) {} // eslint-disable-line no-empty
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class MemoryNavigation {
|
|
29
|
+
constructor(initialLocation, { save, load } = {}) {
|
|
30
|
+
this._save = save;
|
|
31
|
+
|
|
32
|
+
this._keyPrefix = Date.now().toString(36);
|
|
33
|
+
this._keyIndex = 0;
|
|
34
|
+
|
|
35
|
+
this._subscriptionListener = null;
|
|
36
|
+
|
|
37
|
+
const initialState = load
|
|
38
|
+
? _loadState(load, this._isValidLoadedData)
|
|
39
|
+
: null;
|
|
40
|
+
if (initialState) {
|
|
41
|
+
this._stack = initialState.stack;
|
|
42
|
+
this._index = initialState.index;
|
|
43
|
+
} else {
|
|
44
|
+
this._stack = [
|
|
45
|
+
{
|
|
46
|
+
...normalizeInputLocation(initialLocation),
|
|
47
|
+
key: this._getNextKey(),
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
this._index = 0;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
_isValidLoadedData({ stack, index }) {
|
|
55
|
+
// Check that the `stack` and `index` at least seem reasonable before using them.
|
|
56
|
+
// This isn't foolproof, but it might prevent mistakes.
|
|
57
|
+
return Array.isArray(stack) && typeof index === 'number' && stack[index];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
init() {
|
|
61
|
+
return this._createLocationObject({ action: 'INIT', delta: 0 });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
subscribe(listener) {
|
|
65
|
+
this._subscriptionListener = listener;
|
|
66
|
+
|
|
67
|
+
return () => {
|
|
68
|
+
this._subscriptionListener = null;
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
navigate(location) {
|
|
73
|
+
const { action, pathname, search, query, hash, state } = location;
|
|
74
|
+
|
|
75
|
+
if (action !== 'PUSH' && action !== 'REPLACE') {
|
|
76
|
+
throw Error(`Unrecognized browser session action: ${action}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const delta = action === 'PUSH' ? 1 : 0;
|
|
80
|
+
this._index += delta;
|
|
81
|
+
|
|
82
|
+
const key = this._getNextKey();
|
|
83
|
+
|
|
84
|
+
this._stack[this._index] = { pathname, search, query, hash, state, key };
|
|
85
|
+
if (action === 'PUSH') {
|
|
86
|
+
this._stack.length = this._index + 1;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (this._save) {
|
|
90
|
+
_saveState(this._save, {
|
|
91
|
+
stack: this._stack,
|
|
92
|
+
index: this._index,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { ...location, key, index: this._index, delta };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
shift(delta) {
|
|
100
|
+
const prevIndex = this._index;
|
|
101
|
+
|
|
102
|
+
this._index = Math.min(
|
|
103
|
+
Math.max(this._index + delta, 0),
|
|
104
|
+
this._stack.length - 1,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
if (this._index === prevIndex) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (this._save) {
|
|
112
|
+
_saveState(this._save, {
|
|
113
|
+
stack: this._stack,
|
|
114
|
+
index: this._index,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (this._subscriptionListener) {
|
|
119
|
+
this._subscriptionListener(
|
|
120
|
+
this._createLocationObject({
|
|
121
|
+
action: 'POP',
|
|
122
|
+
delta: this._index - prevIndex,
|
|
123
|
+
}),
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
_getNextKey() {
|
|
129
|
+
const key = `${this._keyPrefix}.${this._keyIndex.toString(36)}`;
|
|
130
|
+
this._keyIndex++;
|
|
131
|
+
return key;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
_createLocationObject({ action, delta }) {
|
|
135
|
+
return {
|
|
136
|
+
...this._stack[this._index],
|
|
137
|
+
action,
|
|
138
|
+
index: this._index,
|
|
139
|
+
delta,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
class MemoryDataStorage {
|
|
145
|
+
constructor({ load, save } = {}) {
|
|
146
|
+
this._save = save;
|
|
147
|
+
|
|
148
|
+
const initialState = load
|
|
149
|
+
? _loadState(load, this._isValidLoadedData)
|
|
150
|
+
: null;
|
|
151
|
+
if (initialState) {
|
|
152
|
+
this._state = initialState.state;
|
|
153
|
+
} else {
|
|
154
|
+
this._state = {};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Returns either a `string` value or `null` if the key doesn't exist.
|
|
159
|
+
get(key) {
|
|
160
|
+
if (key in this._state) {
|
|
161
|
+
return this._state[key];
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
remove(key) {
|
|
167
|
+
if (key in this._state) {
|
|
168
|
+
delete this._state[key];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (this._save) {
|
|
172
|
+
_saveState(this._save, {
|
|
173
|
+
state: this._state,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
set(key, value) {
|
|
179
|
+
this._state[key] = value;
|
|
180
|
+
|
|
181
|
+
if (this._save) {
|
|
182
|
+
_saveState(this._save, {
|
|
183
|
+
state: this._state,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
_isValidLoadedData({ state }) {
|
|
189
|
+
// Perform a basic validation of `state`.
|
|
190
|
+
return typeof state === 'object' && state !== null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function createNestedStateSaveLoadFunctions({ save, load }, key) {
|
|
195
|
+
return {
|
|
196
|
+
save: save ? (data) => save(key, data) : undefined,
|
|
197
|
+
load: load ? () => load(key) : undefined,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export default class MemorySession {
|
|
202
|
+
constructor(initialLocation, { save, load } = {}) {
|
|
203
|
+
this.navigation = new MemoryNavigation(
|
|
204
|
+
initialLocation,
|
|
205
|
+
createNestedStateSaveLoadFunctions({ save, load }, 'navigation'),
|
|
206
|
+
);
|
|
207
|
+
this.dataStorage = new MemoryDataStorage(
|
|
208
|
+
createNestedStateSaveLoadFunctions({ save, load }, 'dataStorage'),
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// "Before destroy" listeners are currently ignored.
|
|
213
|
+
// If required, one could implement a `_destroy()` method
|
|
214
|
+
// and there check that the listeners actually do get called.
|
|
215
|
+
// eslint-disable-next-line no-unused-vars
|
|
216
|
+
addBeforeDestroyListener(listener) {
|
|
217
|
+
return () => {};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* eslint-disable max-classes-per-file */
|
|
2
|
+
|
|
1
3
|
import normalizeInputLocation from '../normalizeInputLocation';
|
|
2
4
|
|
|
3
5
|
function noop() {}
|
|
@@ -6,49 +8,60 @@ function serverSideNavigationNotPossible() {
|
|
|
6
8
|
throw new Error('Server-side navigation is not possible');
|
|
7
9
|
}
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
class ServerNavigation {
|
|
10
12
|
constructor(initialLocation) {
|
|
11
13
|
this._location = normalizeInputLocation(initialLocation);
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
init() {
|
|
15
17
|
return {
|
|
16
|
-
action: '
|
|
18
|
+
action: 'INIT',
|
|
17
19
|
...this._location,
|
|
20
|
+
index: 0,
|
|
21
|
+
key: '0',
|
|
18
22
|
};
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
subscribe() {
|
|
22
|
-
// Server environment emits no location events.
|
|
26
|
+
// Server-side environment emits no location subscription events.
|
|
23
27
|
return noop;
|
|
24
28
|
}
|
|
25
29
|
|
|
26
|
-
// Navigation methods are not implemented, because `
|
|
30
|
+
// Navigation methods are not implemented, because `ServerSession` instances
|
|
27
31
|
// cannot navigate.
|
|
28
32
|
navigate() {
|
|
29
33
|
serverSideNavigationNotPossible();
|
|
30
34
|
}
|
|
31
35
|
|
|
32
|
-
// Navigation methods are not implemented, because `
|
|
36
|
+
// Navigation methods are not implemented, because `ServerSession` instances
|
|
33
37
|
// cannot navigate.
|
|
34
38
|
shift() {
|
|
35
39
|
serverSideNavigationNotPossible();
|
|
36
40
|
}
|
|
41
|
+
}
|
|
37
42
|
|
|
38
|
-
|
|
39
|
-
// If required, one could implement a `_destroy()` method
|
|
40
|
-
// and there check that the listeners actually do get called.
|
|
41
|
-
addBeforeDestroyListener() {
|
|
42
|
-
return () => {};
|
|
43
|
-
}
|
|
44
|
-
|
|
43
|
+
class ServerDataStorage {
|
|
45
44
|
// It doesn't seem to make any sense to store anything on server side.
|
|
46
45
|
// Hence, state management methods are "no op" stubs.
|
|
47
|
-
|
|
46
|
+
get() {
|
|
48
47
|
return null;
|
|
49
48
|
}
|
|
50
49
|
|
|
51
|
-
|
|
50
|
+
remove() {}
|
|
51
|
+
|
|
52
|
+
set() {}
|
|
53
|
+
}
|
|
52
54
|
|
|
53
|
-
|
|
55
|
+
export default class ServerSession {
|
|
56
|
+
constructor(initialLocation) {
|
|
57
|
+
this.navigation = new ServerNavigation(initialLocation);
|
|
58
|
+
this.dataStorage = new ServerDataStorage();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// "Before destroy" listeners are currently ignored.
|
|
62
|
+
// If required, one could implement a `_destroy()` method
|
|
63
|
+
// and there check that the listeners actually do get called.
|
|
64
|
+
addBeforeDestroyListener() {
|
|
65
|
+
return noop;
|
|
66
|
+
}
|
|
54
67
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import LocationDataStorage from '../src/LocationDataStorage';
|
|
2
|
+
import MemorySession from '../src/session/MemorySession';
|
|
3
3
|
|
|
4
|
-
describe('
|
|
5
|
-
let
|
|
4
|
+
describe('LocationDataStorage', () => {
|
|
5
|
+
let session;
|
|
6
6
|
let stateStorage;
|
|
7
7
|
|
|
8
8
|
const location = {
|
|
@@ -12,8 +12,8 @@ describe('LocationStateStorage', () => {
|
|
|
12
12
|
beforeEach(() => {
|
|
13
13
|
window.sessionStorage.clear();
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
stateStorage = new
|
|
15
|
+
session = new MemorySession('/initial-location');
|
|
16
|
+
stateStorage = new LocationDataStorage(session, {
|
|
17
17
|
namespace: 'test',
|
|
18
18
|
});
|
|
19
19
|
});
|
|
@@ -2,8 +2,8 @@ import { applyMiddleware, createStore } from 'redux';
|
|
|
2
2
|
|
|
3
3
|
import Actions from '../src/Actions';
|
|
4
4
|
import createMiddlewares from '../src/createMiddlewares';
|
|
5
|
-
import MemoryEnvironment from '../src/environment/MemoryEnvironment';
|
|
6
5
|
import locationReducer from '../src/locationReducer';
|
|
6
|
+
import MemorySession from '../src/session/MemorySession';
|
|
7
7
|
|
|
8
8
|
describe('createMiddlewares', () => {
|
|
9
9
|
let store;
|
|
@@ -11,7 +11,7 @@ describe('createMiddlewares', () => {
|
|
|
11
11
|
beforeEach(() => {
|
|
12
12
|
store = createStore(
|
|
13
13
|
locationReducer,
|
|
14
|
-
applyMiddleware(...createMiddlewares(new
|
|
14
|
+
applyMiddleware(...createMiddlewares(new MemorySession('/foo'))),
|
|
15
15
|
);
|
|
16
16
|
store.dispatch(Actions.init());
|
|
17
17
|
});
|
package/test/helpers.js
CHANGED
|
@@ -23,7 +23,7 @@ export function transformInputLocationUsingMiddleware(middleware, location) {
|
|
|
23
23
|
}).payload;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
export function
|
|
26
|
+
export function transformSubscriptionLocationUsingMiddleware(
|
|
27
27
|
middleware,
|
|
28
28
|
location,
|
|
29
29
|
) {
|
package/test/index.test.js
CHANGED
|
@@ -13,8 +13,8 @@ describe('index', () => {
|
|
|
13
13
|
expect(exports.parseLocationUrl).to.exist();
|
|
14
14
|
expect(exports.createMiddlewares).to.exist();
|
|
15
15
|
expect(exports.locationReducer).to.exist();
|
|
16
|
-
expect(exports.
|
|
17
|
-
expect(exports.
|
|
18
|
-
expect(exports.
|
|
16
|
+
expect(exports.BrowserSession).to.exist();
|
|
17
|
+
expect(exports.MemorySession).to.exist();
|
|
18
|
+
expect(exports.ServerSession).to.exist();
|
|
19
19
|
});
|
|
20
20
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import createBasePathMiddleware from '../../src/middleware/createBasePathMiddleware';
|
|
2
2
|
import {
|
|
3
|
-
transformEnvironmentLocationUsingMiddleware,
|
|
4
3
|
transformInputLocationUsingMiddleware,
|
|
4
|
+
transformSubscriptionLocationUsingMiddleware,
|
|
5
5
|
} from '../helpers';
|
|
6
6
|
|
|
7
7
|
describe('createBasePathMiddleware', () => {
|
|
@@ -22,9 +22,9 @@ describe('createBasePathMiddleware', () => {
|
|
|
22
22
|
});
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
it('should strip `basePath` from `location.pathname` on
|
|
25
|
+
it('should strip `basePath` from `location.pathname` on subscription locations', () => {
|
|
26
26
|
expect(
|
|
27
|
-
|
|
27
|
+
transformSubscriptionLocationUsingMiddleware(basePathMiddleware, {
|
|
28
28
|
pathname: '/foo/path',
|
|
29
29
|
}),
|
|
30
30
|
).to.eql({
|
|
@@ -32,9 +32,9 @@ describe('createBasePathMiddleware', () => {
|
|
|
32
32
|
});
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
-
it('should handle unrecognized paths on
|
|
35
|
+
it('should handle unrecognized paths on subscription locations', () => {
|
|
36
36
|
expect(
|
|
37
|
-
|
|
37
|
+
transformSubscriptionLocationUsingMiddleware(basePathMiddleware, {
|
|
38
38
|
pathname: '/bar/path',
|
|
39
39
|
}),
|
|
40
40
|
).to.eql({
|
|
@@ -54,10 +54,10 @@ describe('createBasePathMiddleware', () => {
|
|
|
54
54
|
).to.equal(location);
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
-
it('should not modify `location.pathname` of
|
|
57
|
+
it('should not modify `location.pathname` of subscription locations', () => {
|
|
58
58
|
const location = { pathname: '/path' };
|
|
59
59
|
expect(
|
|
60
|
-
|
|
60
|
+
transformSubscriptionLocationUsingMiddleware(
|
|
61
61
|
basePathMiddleware,
|
|
62
62
|
location,
|
|
63
63
|
),
|