@webqit/webflo 0.8.72 → 0.8.75

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/package.json CHANGED
@@ -12,7 +12,7 @@
12
12
  "vanila-javascript"
13
13
  ],
14
14
  "homepage": "https://webqit.io/tooling/webflo",
15
- "version": "0.8.72",
15
+ "version": "0.8.75",
16
16
  "license": "MIT",
17
17
  "repository": {
18
18
  "type": "git",
@@ -45,22 +45,24 @@ export default class Http {
45
45
  * @return void
46
46
  */
47
47
  async go(url, options = {}) {
48
- if (this.activeController) {
49
- this.activeController.abort();
48
+ if (this.abortController) {
49
+ this.abortController.abort();
50
50
  }
51
- this.activeController = new AbortController();
51
+ this.abortController = new AbortController();
52
+ let xRedirectCode = 300;
52
53
  // Generates request object
53
54
  let generateRequest = (url, options) => {
54
55
  return new StdRequest(url, {
55
56
  ...options,
56
57
  headers: {
57
58
  'Accept': 'application/json',
58
- 'X-No-Cors-Redirect': 'manual',
59
+ 'X-Redirect-Policy': 'manual-when-cross-origin',
60
+ 'X-Redirect-Code': xRedirectCode,
59
61
  'X-Powered-By': '@webqit/webflo',
60
62
  ...(options.headers || {}),
61
63
  },
62
64
  referrer: window.document.location.href,
63
- signal: this.activeController.signal,
65
+ signal: this.abortController.signal,
64
66
  });
65
67
  };
66
68
  // Handles response object
@@ -70,14 +72,14 @@ export default class Http {
70
72
  Observer.set(this.location, { href: response.url }, {
71
73
  detail: { isRedirect: true },
72
74
  });
73
- } else if (response.headers.get('X-No-Cors-Redirect')) {
75
+ } else if (response.headers.get('Location') && response.status === xRedirectCode) {
74
76
  window.location = response.headers.get('Location');
75
77
  }
76
78
  };
77
79
  url = typeof url === 'string' ? { href: url } : url;
78
- options = { referrer: window.document.location.href, ...options };
80
+ options = { referrer: this.location.href, ...options };
79
81
  Observer.set(this.location, url, { detail: options, });
80
- if (options.srcType === 'history' || !(_before(url.href, '#') === _before(options.referrer, '#') && (options.method || 'GET').toUpperCase() === 'GET')) {
82
+ if (!(_before(url.href, '#') === _before(options.referrer, '#') && (options.method || 'GET').toUpperCase() === 'GET')) {
81
83
  handleResponse(await client.call(this, generateRequest(url.href, options)));
82
84
  }
83
85
  },
@@ -108,7 +110,13 @@ export default class Http {
108
110
  };
109
111
 
110
112
  // -----------------------
111
- // Initialize instance
113
+ // Initialize network
114
+ Observer.set(instance, 'network', {});
115
+ window.addEventListener('online', () => Observer.set(instance.network, 'online', navigator.onLine));
116
+ window.addEventListener('offline', () => Observer.set(instance.network, 'online', navigator.onLine));
117
+
118
+ // -----------------------
119
+ // Initialize location
112
120
  Observer.set(instance, 'location', new Url(window.document.location));
113
121
  // -----------------------
114
122
  // Syndicate changes to the browser;s location bar
@@ -169,14 +177,14 @@ export default class Http {
169
177
  // Capture all form-submit
170
178
  // and fire to this router.
171
179
  window.addEventListener('submit', e => {
172
- var form = e.target.closest('form'),
173
- submits = [e.submitter]; //_arrFrom(form.elements).filter(el => el.matches('button,input[type="submit"],input[type="image"]'));
180
+ var form = e.target.closest('form'), submitter = e.submitter;
174
181
  var submitParams = [ 'action', 'enctype', 'method', 'noValidate', 'target' ].reduce((params, prop) => {
175
- params[prop] = submits.reduce((val, el) => val || (el.hasAttribute(`form${prop.toLowerCase()}`) ? el[`form${_toTitle(prop)}`] : null), null) || form[prop];
182
+ params[prop] = submitter && submitter.hasAttribute(`form${prop.toLowerCase()}`) ? submitter[`form${_toTitle(prop)}`] : form[prop];
176
183
  return params;
177
184
  }, {});
178
185
  // We support method hacking
179
- submitParams.method = e.submitter.dataset.method || form.dataset.method || submitParams.method;
186
+ submitParams.method = (submitter && submitter.dataset.method) || form.dataset.method || submitParams.method;
187
+ submitParams.submitter = submitter;
180
188
  // ---------------
181
189
  var actionEl = window.document.createElement('a');
182
190
  actionEl.href = submitParams.action;
@@ -186,8 +194,8 @@ export default class Http {
186
194
  if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return;
187
195
  // Build data
188
196
  var formData = new FormData(form);
189
- if (e.submitter.name) {
190
- formData.set(e.submitter.name, e.submitter.value);
197
+ if ((submitter || {}).name) {
198
+ formData.set(submitter.name, submitter.value);
191
199
  }
192
200
  if (submitParams.method.toUpperCase() === 'GET') {
193
201
  var query = wwwFormUnserialize(actionEl.search);
@@ -0,0 +1,250 @@
1
+
2
+ /**
3
+ * @imports
4
+ */
5
+ import _before from '@webqit/util/str/before.js';
6
+ import _toTitle from '@webqit/util/str/toTitle.js';
7
+ import { wwwFormUnserialize, wwwFormSet, wwwFormSerialize } from '../util.js';
8
+ import { Observer } from './Runtime.js';
9
+ import Url from './Url.js';
10
+
11
+ export default class Navigator {
12
+
13
+ constructor(client) {
14
+ this.client = client;
15
+
16
+ /**
17
+ * ----------------
18
+ * Navigator location
19
+ * ----------------
20
+ */
21
+
22
+ // -----------------------
23
+ // Initialize location
24
+ Observer.set(this, 'location', new Url(window.document.location));
25
+ // -----------------------
26
+ // Syndicate changes to the browser;s location bar
27
+ Observer.observe(this.location, [[ 'href' ]], ([e]) => {
28
+ if (e.value === 'http:' || (e.detail || {}).src === window.document.location) {
29
+ // Already from a "popstate" event as above, so don't push again
30
+ return;
31
+ }
32
+ if (e.value === window.document.location.href || e.value + '/' === window.document.location.href) {
33
+ window.history.replaceState(window.history.state, '', this.location.href);
34
+ } else {
35
+ try { window.history.pushState(window.history.state, '', this.location.href); } catch(e) {}
36
+ }
37
+ }, { diff: true });
38
+
39
+ // -----------------------
40
+ // This event is triggered by
41
+ // either the browser back button,
42
+ // the window.history.back(),
43
+ // the window.history.forward(),
44
+ // or the window.history.go() action.
45
+ window.addEventListener('popstate', e => {
46
+ // Needed to allow window.document.location
47
+ // to update to window.location
48
+ window.setTimeout(() => {
49
+ this.go(Url.copy(window.document.location), { src: window.document.location, srcType: 'history', });
50
+ }, 0);
51
+ });
52
+
53
+ // -----------------------
54
+ // Capture all link-clicks
55
+ // and fire to this router.
56
+ window.addEventListener('click', e => {
57
+ var anchor = e.target.closest('a');
58
+ if (!anchor || !anchor.href) return;
59
+ if (!anchor.target && !anchor.download && (!anchor.origin || anchor.origin === this.location.origin)) {
60
+ if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return;
61
+ // Publish everything, including hash
62
+ this.go(Url.copy(anchor), { src: anchor, srcType: 'link', });
63
+ // URLs with # will cause a natural navigation
64
+ // even if pointing to a different page, a natural navigation will still happen
65
+ // because with the Observer.set() above, window.document.location.href would have become
66
+ // the destination page, which makes it look like same page navigation
67
+ if (!anchor.href.includes('#')) {
68
+ e.preventDefault();
69
+ }
70
+ }
71
+ });
72
+
73
+ // -----------------------
74
+ // Capture all form-submit
75
+ // and fire to this router.
76
+ window.addEventListener('submit', e => {
77
+ var form = e.target.closest('form'), submitter = e.submitter;
78
+ var submitParams = [ 'action', 'enctype', 'method', 'noValidate', 'target' ].reduce((params, prop) => {
79
+ params[prop] = submitter && submitter.hasAttribute(`form${prop.toLowerCase()}`) ? submitter[`form${_toTitle(prop)}`] : form[prop];
80
+ return params;
81
+ }, {});
82
+ // We support method hacking
83
+ submitParams.method = (submitter && submitter.dataset.method) || form.dataset.method || submitParams.method;
84
+ submitParams.submitter = submitter;
85
+ // ---------------
86
+ var actionEl = window.document.createElement('a');
87
+ actionEl.href = submitParams.action;
88
+ // ---------------
89
+ // If not targeted and same origin...
90
+ if (!submitParams.target && (!actionEl.origin || actionEl.origin === this.location.origin)) {
91
+ if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return;
92
+ // Build data
93
+ var formData = new FormData(form);
94
+ if ((submitter || {}).name) {
95
+ formData.set(submitter.name, submitter.value);
96
+ }
97
+ if (submitParams.method.toUpperCase() === 'GET') {
98
+ var query = wwwFormUnserialize(actionEl.search);
99
+ Array.from(formData.entries()).forEach(_entry => {
100
+ wwwFormSet(query, _entry[0], _entry[1], false);
101
+ });
102
+ actionEl.search = wwwFormSerialize(query);
103
+ formData = null;
104
+ }
105
+ this.go(Url.copy(actionEl), { ...submitParams, body: formData, src: form, srcType: 'form', });
106
+ // URLs with # will cause a natural navigation
107
+ // even if pointing to a different page, a natural navigation will still happen
108
+ // because with the Observer.set() above, window.document.location.href would have become
109
+ // the destination page, which makes it look like same page navigation
110
+ if (!actionEl.hash) {
111
+ e.preventDefault();
112
+ }
113
+ }
114
+ });
115
+
116
+ /**
117
+ * ----------------
118
+ * Navigator network
119
+ * ----------------
120
+ */
121
+
122
+ // -----------------------
123
+ // Initialize network
124
+ Observer.set(this, 'network', {});
125
+ window.addEventListener('online', () => Observer.set(this.network, 'online', navigator.onLine));
126
+ window.addEventListener('offline', () => Observer.set(this.network, 'online', navigator.onLine));
127
+
128
+ /**
129
+ * ----------------
130
+ * Initial navigation
131
+ * ----------------
132
+ */
133
+
134
+ this.go(this.location, { srcType: 'init' });
135
+ }
136
+
137
+ /**
138
+ * History object
139
+ */
140
+ get history() {
141
+ return window.history;
142
+ }
143
+
144
+ /**
145
+ * Performs a request.
146
+ *
147
+ * @param object|string href
148
+ * @param object params
149
+ *
150
+ * @return void
151
+ */
152
+ async go(url, params = {}) {
153
+
154
+ // Generates request object
155
+ const generateRequest = (url, params) => {
156
+ return new Request(url, {
157
+ ...params,
158
+ headers: {
159
+ 'Accept': 'application/json',
160
+ 'Cache-Control': 'no-store',
161
+ 'X-Redirect-Policy': 'manual-when-cross-origin',
162
+ 'X-Redirect-Code': xRedirectCode,
163
+ 'X-Powered-By': '@webqit/webflo',
164
+ ...(params.headers || {}),
165
+ },
166
+ referrer: window.document.location.href,
167
+ signal: this._abortController.signal,
168
+ });
169
+ };
170
+
171
+ // Initiates remote fetch and sets the status
172
+ const remoteRequest = request => {
173
+ Observer.set(this.network, 'remote', true);
174
+ let _response = fetch(request);
175
+ // This catch() is NOT intended to handle failure of the fetch
176
+ _response.catch(e => Observer.set(this.network, 'error', e.message));
177
+ // Save a reference to this
178
+ return _response.then(async response => {
179
+ // Stop loading status
180
+ Observer.set(this.network, 'remote', false);
181
+ return response;
182
+ });
183
+ };
184
+
185
+ // Handles response object
186
+ const handleResponse = async (response, params) => {
187
+ response = await response;
188
+ Observer.set(this.network, 'remote', false);
189
+ Observer.set(this.network, 'error', null);
190
+ if (['link', 'form'].includes(params.srcType)) {
191
+ Observer.set(params.src, 'active', false);
192
+ Observer.set(params.submitter || {}, 'active', false);
193
+ }
194
+ if (!response) return;
195
+ if (response.redirected && this.isSameOrigin(response.url)) {
196
+ Observer.set(this.location, { href: response.url }, {
197
+ detail: { isRedirect: true },
198
+ });
199
+ return;
200
+ }
201
+ let location = response.headers.get('Location');
202
+ if (location && response.status === xRedirectCode) {
203
+ Observer.set(this.network, 'redirecting', location);
204
+ window.location = location;
205
+ }
206
+ };
207
+
208
+ // ------------
209
+ url = typeof url === 'string' ? { href: url } : url;
210
+ params = { referrer: this.location.href, ...params };
211
+ // ------------
212
+ Observer.set(this.location, url, { detail: params, });
213
+ Observer.set(this.network, 'redirecting', null);
214
+ // ------------
215
+ if (['link', 'form'].includes(params.srcType)) {
216
+ Observer.set(params.src, 'active', true);
217
+ Observer.set(params.submitter || {}, 'active', true);
218
+ }
219
+ // ------------
220
+
221
+ if (this._abortController) {
222
+ this._abortController.abort();
223
+ }
224
+ this._abortController = new AbortController();
225
+ let xRedirectCode = 300;
226
+
227
+ if (params.srcType === 'init' || !(_before(url.href, '#') === _before(params.referrer, '#') && (params.method || 'GET').toUpperCase() === 'GET')) {
228
+ handleResponse(this.client.call(this, generateRequest(url.href, params), params, remoteRequest), params);
229
+ }
230
+
231
+ return this._abortController;
232
+ }
233
+
234
+ /**
235
+ * Checks if an URL is same origin.
236
+ *
237
+ * @param object|string url
238
+ *
239
+ * @return Bool
240
+ */
241
+ isSameOrigin(url) {
242
+ if (typeof url === 'string') {
243
+ let href = url;
244
+ url = window.document.createElement('a');
245
+ url.href = href
246
+ }
247
+ return !url.origin || url.origin === this.location.origin;
248
+ }
249
+
250
+ }
@@ -10,7 +10,7 @@ import NavigationEvent from './NavigationEvent.js';
10
10
  import WorkerClient from './WorkerClient.js';
11
11
  import Storage from './Storage.js';
12
12
  import Router from './Router.js';
13
- import Http from './Http.js';
13
+ import Navigator from './Navigator.js';
14
14
 
15
15
  /**
16
16
  * ---------------------------
@@ -21,184 +21,106 @@ import Http from './Http.js';
21
21
  export const { Observer } = window.WebQit;
22
22
  export default function(layout, params) {
23
23
 
24
- const session = Storage();
25
- const workerClient = new WorkerClient('/worker.js', { startMessages: true });
26
- Observer.observe(workerClient, changes => {
27
- console.log('SERVICE_WORKER_STATE', changes[0].name, changes[0].value);
28
- });
29
-
30
- // Copy..
31
24
  layout = {...layout};
32
25
  params = {...params};
33
- window.addEventListener('online', () => Observer.set(networkWatch, 'online', navigator.onLine));
34
- window.addEventListener('offline', () => Observer.set(networkWatch, 'online', navigator.onLine));
35
- var networkProgressOngoing;
36
-
37
- /**
38
- * ----------------
39
- * Apply routing
40
- * ----------------
41
- */
42
-
43
- Http.createClient(async function(request, event = null) {
44
26
 
45
- const httpInstance = this;
46
-
47
- // -------------------
48
- // Resolve canonicity
49
- // -------------------
50
-
51
- // The srvice object
52
- const $context = {
53
- layout,
54
- onHydration: !event && (await window.WebQit.OOHTML.meta.get('isomorphic')),
55
- response: null,
56
- }
27
+ const session = Storage();
28
+ const workerClient = new WorkerClient('/worker.js', { startMessages: true });
29
+ const navigator = new Navigator(async (request, params, remoteFetch) => {
57
30
 
58
- // The app router
59
- const clientNavigationEvent = new NavigationEvent(request, session);
60
- const requestPath = clientNavigationEvent.url.pathname;
61
- const router = new Router(requestPath, layout, $context);
62
- if (networkProgressOngoing) {
63
- networkProgressOngoing.setActive(false);
64
- networkProgressOngoing = null;
65
- }
31
+ // The navigation event
32
+ const clientNavigationEvent = new NavigationEvent(
33
+ new NavigationEvent.Request(request),
34
+ session,
35
+ );
66
36
 
67
- try {
37
+ // The app router
38
+ const router = new Router(clientNavigationEvent.url.pathname, layout, {
39
+ layout,
40
+ onHydration: params.srcType === 'init',
41
+ });
68
42
 
69
- // --------
70
- // ROUTE FOR DATA
71
- // --------
72
- const httpMethodName = clientNavigationEvent.request.method.toLowerCase();
73
- $context.response = await router.route([httpMethodName === 'delete' ? 'del' : httpMethodName, 'default'], clientNavigationEvent, document.state, async function(event) {
74
- // -----------------
75
- var networkProgress = networkProgressOngoing = new RequestHandle();
76
- networkProgress.setActive(true, event.request._method || event.request.method);
77
- const _response = fetch(event.request);
78
- // This catch() is NOT intended to handle failure of the fetch
79
- _response.catch(e => networkProgress.throw(e.message));
80
- // Save a reference to this
81
- return _response.then(async response => {
82
- response = new clientNavigationEvent.Response(response.body, {
83
- status: response.status,
84
- statusText: response.statusText,
85
- headers: response.headers,
86
- _proxy: {
87
- url: response.url,
88
- ok: response.ok,
89
- redirected: response.redirected
90
- },
91
- });
92
- if (response.headers.get('Location')) {
93
- networkProgress.redirecting(response.headers.get('Location'));
94
- }
95
- // Stop loading status
96
- networkProgress.setActive(false);
97
- return response;
43
+ // --------
44
+ // ROUTE FOR DATA
45
+ // --------
46
+ const httpMethodName = clientNavigationEvent.request.method.toLowerCase();
47
+ const response = await router.route([httpMethodName === 'delete' ? 'del' : httpMethodName, 'default'], clientNavigationEvent, document.state, async function(event) {
48
+ return remoteFetch(event.request).then(response => {
49
+ return new clientNavigationEvent.Response(response.body, {
50
+ status: response.status,
51
+ statusText: response.statusText,
52
+ headers: response.headers,
53
+ _proxy: {
54
+ url: response.url,
55
+ ok: response.ok,
56
+ redirected: response.redirected
57
+ },
98
58
  });
99
59
  });
100
- if ($context.response instanceof clientNavigationEvent.Response) {
101
- $context.data = await $context.response.data();
102
- } else {
103
- $context.data = $context.response;
104
- }
60
+ }).catch(e => {
61
+ window.document.body.setAttribute('template', '');
62
+ throw e;
63
+ });
105
64
 
106
- // --------
107
- // Render
108
- // --------
109
- const rendering = await router.route('render', clientNavigationEvent, $context.data, async function(event, data) {
110
- // --------
111
- // OOHTML would waiting for DOM-ready in order to be initialized
112
- await new Promise(res => window.WebQit.DOM.ready(res));
113
- if (!window.document.state.env) {
114
- window.document.setState({
115
- env: 'client',
116
- onHydration: $context.onHydration,
117
- network: networkWatch,
118
- url: httpInstance.location,
119
- session,
120
- }, { update: true });
121
- }
122
- window.document.setState({ page: data }, { update: 'merge' });
123
- window.document.body.setAttribute('template', 'page/' + requestPath.split('/').filter(a => a).map(a => a + '+-').join('/'));
124
- return new Promise(res => {
125
- window.document.addEventListener('templatesreadystatechange', () => res(window));
126
- if (window.document.templatesReadyState === 'complete') {
127
- res(window);
128
- }
129
- });
130
- });
131
65
 
66
+ // --------
67
+ // Render
68
+ // --------
69
+ const data = response instanceof clientNavigationEvent.Response ? await response.data() : response;
70
+ await router.route('render', clientNavigationEvent, data, async function(event, data) {
132
71
  // --------
133
- // Render...
134
- // --------
135
-
136
- if (/*document.activeElement === document.body && */event && _isObject(event.detail) && (event.detail.src instanceof Element) && /* do only on url path change */ _before(event.value, '?') !== _before(event.oldValue, '?')) {
137
- setTimeout(() => {
138
- let vieportTop;
139
- if (clientNavigationEvent.url.hash && (urlTarget = document.querySelector(clientNavigationEvent.url.hash))) {
140
- urlTarget.scrollIntoView();
141
- } else if (vieportTop = Array.from(document.querySelectorAll('[data-viewport-top]')).pop()) {
142
- vieportTop.focus();
143
- } else {
144
- document.documentElement.classList.add('scroll-reset');
145
- document.body.scrollIntoView();
146
- setTimeout(() => {
147
- document.documentElement.classList.remove('scroll-reset');
148
- }, 600);
149
- }
150
- }, 0);
72
+ // OOHTML would waiting for DOM-ready in order to be initialized
73
+ await new Promise(res => window.WebQit.DOM.ready(res));
74
+ if (!window.document.state.env) {
75
+ window.document.setState({
76
+ env: 'client',
77
+ onHydration: params.srcType === 'init',
78
+ network: navigator.network,
79
+ url: navigator.location,
80
+ session,
81
+ }, { update: true });
151
82
  }
83
+ window.document.setState({ page: data }, { update: 'merge' });
84
+ window.document.body.setAttribute('template', 'page/' + clientNavigationEvent.url.pathname.split('/').filter(a => a).map(a => a + '+-').join('/'));
85
+ return new Promise(res => {
86
+ window.document.addEventListener('templatesreadystatechange', () => res(window));
87
+ if (window.document.templatesReadyState === 'complete') {
88
+ res(window);
89
+ }
90
+ });
91
+ });
152
92
 
153
- } catch(e) {
154
-
155
- window.document.body.setAttribute('template', '');
156
- throw e;
157
-
93
+ // --------
94
+ // Render...
95
+ // --------
96
+
97
+ if (params.src instanceof Element) {
98
+ setTimeout(() => {
99
+ let viewportTop;
100
+ if (clientNavigationEvent.url.hash && (urlTarget = document.querySelector(clientNavigationEvent.url.hash))) {
101
+ urlTarget.scrollIntoView();
102
+ } else if (viewportTop = Array.from(document.querySelectorAll('[data-viewport-top]')).pop()) {
103
+ viewportTop.focus();
104
+ } else {
105
+ document.documentElement.classList.add('scroll-reset');
106
+ document.body.scrollIntoView();
107
+ setTimeout(() => {
108
+ document.documentElement.classList.remove('scroll-reset');
109
+ }, 600);
110
+ }
111
+ }, 0);
158
112
  }
159
113
 
160
- return $context.response;
114
+ return response;
161
115
  });
162
116
 
117
+ Observer.observe(session, changes => {
118
+ //console.log('SESSION_STATE_CHANGE', changes[0].name, changes[0].value);
119
+ });
120
+ Observer.observe(workerClient, changes => {
121
+ //console.log('SERVICE_WORKER_STATE_CHANGE', changes[0].name, changes[0].value);
122
+ });
123
+ Observer.observe(navigator, changes => {
124
+ //console.log('NAVIGATORSTATE_CHANGE', changes[0].name, changes[0].value);
125
+ });
163
126
  };
164
-
165
- const networkWatch = { progress: {}, online: navigator.onLine };
166
- class RequestHandle {
167
- setActive(state, method = '') {
168
- if (this.active === false) {
169
- return;
170
- }
171
- this.active = state;
172
- Observer.set(networkWatch, {
173
- method,
174
- error: '',
175
- progress: {
176
- active: state,
177
- determinate: false,
178
- valuenow: 0,
179
- valuetotal: NaN,
180
- },
181
- });
182
- }
183
- updateProgress(phase, valuenow, valuetotal) {
184
- if (this.active === false) {
185
- return;
186
- }
187
- Observer.set(networkWatch.progress, {
188
- phase,
189
- determinate: !isNaN(valuetotal),
190
- valuenow,
191
- valuetotal,
192
- });
193
- }
194
- redirecting(location) {
195
- Observer.set(networkWatch, 'redirecting', location);
196
- }
197
- throw(message) {
198
- if (this.active === false) {
199
- return;
200
- }
201
- this.error = true;
202
- Observer.set(networkWatch, 'error', message);
203
- }
204
- };
@@ -120,7 +120,8 @@ export default function(layout, params) {
120
120
 
121
121
  return defaultFetch(evt);
122
122
  };
123
- evt.respondWith(handleFetch(evt));
123
+ let response = handleFetch(evt);
124
+ evt.respondWith(response);
124
125
  });
125
126
 
126
127
  const defaultFetch = function(evt) {
@@ -208,7 +208,6 @@ export async function run(hostSetup, request, response, Ui, flags = {}, protocol
208
208
  // --------
209
209
 
210
210
  const fullUrl = protocol + '://' + request.headers.host + request.url;
211
- const _request = new NavigationEvent.Request(fullUrl, requestInit);
212
211
  const _sessionFactory = function(id, params = {}, callback = null) {
213
212
  let factory, secret = hostSetup.variables.entries.SESSION_KEY;
214
213
  Sessions({
@@ -229,8 +228,8 @@ export async function run(hostSetup, request, response, Ui, flags = {}, protocol
229
228
  return !callback ? factory : undefined;
230
229
  };
231
230
  const serverNavigationEvent = new NavigationEvent(
232
- _request,
233
- _sessionFactory('_session', {duration: 60 * 60 * 24 * 30}).get(),
231
+ new NavigationEvent.Request(fullUrl, requestInit),
232
+ _sessionFactory('_session', { duration: 60 * 60, activeDuration: 60 * 60 }).get(),
234
233
  _sessionFactory
235
234
  );
236
235
 
@@ -464,15 +463,17 @@ export async function run(hostSetup, request, response, Ui, flags = {}, protocol
464
463
  // Send
465
464
  // -------------------
466
465
  if ($context.response.headers.redirect) {
467
- if (serverNavigationEvent.request.headers.get('X-No-Cors-Redirect') === 'manual'
468
- && (new serverNavigationEvent.globals.URL($context.response.headers.location)).origin !== serverNavigationEvent.url.origin) {
469
- response.statusCode = 200;
470
- response.setHeader('X-No-Cors-Redirect', $context.response.status);
471
- response.end();
466
+ let xRedirectPolicy = serverNavigationEvent.request.headers.get('X-Redirect-Policy');
467
+ let xRedirectCode = serverNavigationEvent.request.headers.get('X-Redirect-Code') || 300;
468
+ let isSameOriginRedirect = (new serverNavigationEvent.globals.URL($context.response.headers.location)).origin === serverNavigationEvent.url.origin;
469
+ if (xRedirectPolicy === 'manual' || (!isSameOriginRedirect && xRedirectPolicy === 'manual-when-cross-origin') || (isSameOriginRedirect && xRedirectPolicy === 'manual-when-same-origin')) {
470
+ response.statusCode = xRedirectCode;
471
+ response.setHeader('X-Redirect-Code', $context.response.status);
472
+ response.setHeader('Cache-Control', 'no-store');
472
473
  } else {
473
474
  response.statusCode = $context.response.status;
474
- response.end();
475
475
  }
476
+ response.end();
476
477
  } else if ($context.response.original !== undefined && $context.response.original !== null) {
477
478
  response.statusCode = $context.response.status;
478
479
  response.statusMessage = $context.response.statusText;
@@ -553,9 +554,9 @@ export async function run(hostSetup, request, response, Ui, flags = {}, protocol
553
554
 
554
555
  if (flags.logs !== false) {
555
556
  let errorCode = [ 404, 500 ].includes(response.statusCode) ? response.statusCode : 0;
556
- let noCorsRedirect = response.getHeader('X-No-Cors-Redirect');
557
- let redirectCode = noCorsRedirect || ((response.statusCode + '').startsWith('3') ? response.statusCode : 0);
558
- let statusCode = noCorsRedirect || response.statusCode;
557
+ let xRedirectCode = response.getHeader('X-Redirect-Code');
558
+ let redirectCode = xRedirectCode || ((response.statusCode + '').startsWith('3') ? response.statusCode : 0);
559
+ let statusCode = xRedirectCode || response.statusCode;
559
560
  Ui.log(''
560
561
  + '[' + (hostSetup.vh ? Ui.style.keyword(hostSetup.vh.host) + '][' : '') + Ui.style.comment((new Date).toUTCString()) + '] '
561
562
  + Ui.style.keyword(protocol.toUpperCase() + ' ' + serverNavigationEvent.request.method) + ' '