@webqit/webflo 0.8.72-0 → 0.8.72

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-0",
15
+ "version": "0.8.72",
16
16
  "license": "MIT",
17
17
  "repository": {
18
18
  "type": "git",
@@ -106,21 +106,25 @@ const _MessageStream = (NativeMessageStream, Headers, FormData) => {
106
106
  // Payload
107
107
  data(force = false) {
108
108
  if (!this._typedDataCache.data || force) {
109
- this._typedDataCache.data = new Promise(async resolve => {
110
- var request = this, data, contentType = request.headers.get('content-type') || '';
109
+ this._typedDataCache.data = new Promise(async (resolve, reject) => {
110
+ var messageInstance = this, data, contentType = messageInstance.headers.get('content-type') || '';
111
111
  var type = contentType === 'application/json' || this._typedDataCache.json ? 'json' : (
112
- contentType === 'application/x-www-form-urlencoded' || contentType.startsWith('multipart/') || this._typedDataCache.formData || (!contentType && !['get'].includes((request.method || '').toLowerCase())) ? 'formData' : (
112
+ contentType === 'application/x-www-form-urlencoded' || contentType.startsWith('multipart/') || this._typedDataCache.formData ? 'formData' : (
113
113
  contentType === 'text/plain' ? 'plain' : 'other'
114
114
  )
115
115
  );
116
- if (type === 'formData') {
117
- data = (await request.formData()).json();
118
- } else {
119
- data = type === 'json' ? await request.json() : (
120
- type === 'plain' ? await request.text() : request.body
121
- );
116
+ try {
117
+ if (type === 'formData') {
118
+ data = (await messageInstance.formData()).json();
119
+ } else {
120
+ data = type === 'json' ? await messageInstance.json() : (
121
+ type === 'plain' ? await messageInstance.text() : messageInstance.body
122
+ );
123
+ }
124
+ resolve(data);
125
+ } catch(e) {
126
+ reject(e);
122
127
  }
123
- resolve(data);
124
128
  });
125
129
  }
126
130
  return this._typedDataCache.data;
@@ -69,6 +69,14 @@ const _ResponseHeaders = NativeHeaders => class extends _Headers(NativeHeaders)
69
69
  return value;
70
70
  }
71
71
 
72
+ get location() {
73
+ return this.get('Location');
74
+ }
75
+
76
+ set location(value) {
77
+ return this.set('Location', value);
78
+ }
79
+
72
80
  get redirect() {
73
81
  return this.get('Location');
74
82
  }
@@ -76,6 +84,7 @@ const _ResponseHeaders = NativeHeaders => class extends _Headers(NativeHeaders)
76
84
  set redirect(value) {
77
85
  return this.set('Location', value);
78
86
  }
87
+
79
88
  }
80
89
 
81
90
  export default _ResponseHeaders;
@@ -39,25 +39,97 @@ export default class Http {
39
39
  /**
40
40
  * Performs a request.
41
41
  *
42
- * @param string href
43
- * @param object request
44
- * @param object src
42
+ * @param object|string href
43
+ * @param object options
45
44
  *
46
- * @return UserEvent
45
+ * @return void
47
46
  */
48
- go(href, request = {}, src = null) {
49
- return Observer.set(this.location, 'href', href, {
50
- detail: {request, src,},
51
- });
47
+ async go(url, options = {}) {
48
+ if (this.activeController) {
49
+ this.activeController.abort();
50
+ }
51
+ this.activeController = new AbortController();
52
+ // Generates request object
53
+ let generateRequest = (url, options) => {
54
+ return new StdRequest(url, {
55
+ ...options,
56
+ headers: {
57
+ 'Accept': 'application/json',
58
+ 'X-No-Cors-Redirect': 'manual',
59
+ 'X-Powered-By': '@webqit/webflo',
60
+ ...(options.headers || {}),
61
+ },
62
+ referrer: window.document.location.href,
63
+ signal: this.activeController.signal,
64
+ });
65
+ };
66
+ // Handles response object
67
+ let handleResponse = (response) => {
68
+ if (!response) return;
69
+ if (response.redirected && this.isSameOrigin(response.url)) {
70
+ Observer.set(this.location, { href: response.url }, {
71
+ detail: { isRedirect: true },
72
+ });
73
+ } else if (response.headers.get('X-No-Cors-Redirect')) {
74
+ window.location = response.headers.get('Location');
75
+ }
76
+ };
77
+ url = typeof url === 'string' ? { href: url } : url;
78
+ options = { referrer: window.document.location.href, ...options };
79
+ Observer.set(this.location, url, { detail: options, });
80
+ if (options.srcType === 'history' || !(_before(url.href, '#') === _before(options.referrer, '#') && (options.method || 'GET').toUpperCase() === 'GET')) {
81
+ handleResponse(await client.call(this, generateRequest(url.href, options)));
82
+ }
83
+ },
84
+
85
+ /**
86
+ * Checks if an URL is same origin.
87
+ *
88
+ * @param object|string url
89
+ *
90
+ * @return Bool
91
+ */
92
+ isSameOrigin(url) {
93
+ if (typeof url === 'string') {
94
+ let href = url;
95
+ url = window.document.createElement('a');
96
+ url.href = href
97
+ }
98
+ return !url.origin || url.origin === this.location.origin;
99
+ },
100
+
101
+ /**
102
+ * History object
103
+ */
104
+ get history() {
105
+ return window.history;
52
106
  }
107
+
53
108
  };
54
109
 
110
+ // -----------------------
111
+ // Initialize instance
112
+ Observer.set(instance, 'location', new Url(window.document.location));
113
+ // -----------------------
114
+ // Syndicate changes to the browser;s location bar
115
+ Observer.observe(instance.location, [[ 'href' ]], ([e]) => {
116
+ if (e.value === 'http:' || (e.detail || {}).src === window.document.location) {
117
+ // Already from a "popstate" event as above, so don't push again
118
+ return;
119
+ }
120
+ if (e.value === window.document.location.href || e.value + '/' === window.document.location.href) {
121
+ instance.history.replaceState(instance.history.state, '', instance.location.href);
122
+ } else {
123
+ try { instance.history.pushState(instance.history.state, '', instance.location.href); } catch(e) {}
124
+ }
125
+ }, { diff: true });
126
+
55
127
  /**
56
128
  * ----------------
57
- * instance.location
129
+ * Navigation Interception
58
130
  * ----------------
59
131
  */
60
- Observer.set(instance, 'location', new Url(window.document.location));
132
+
61
133
  // -----------------------
62
134
  // This event is triggered by
63
135
  // either the browser back button,
@@ -67,10 +139,9 @@ export default class Http {
67
139
  window.addEventListener('popstate', e => {
68
140
  // Needed to allow window.document.location
69
141
  // to update to window.location
142
+ let referrer = window.document.location.href;
70
143
  window.setTimeout(() => {
71
- Observer.set(instance.location, Url.copy(window.document.location), {
72
- detail: { type: 'history', src: window.document.location },
73
- });
144
+ instance.go(Url.copy(window.document.location), { referrer, src: window.document.location, srcType: 'history', });
74
145
  }, 0);
75
146
  });
76
147
 
@@ -78,24 +149,17 @@ export default class Http {
78
149
  // Capture all link-clicks
79
150
  // and fire to this router.
80
151
  window.addEventListener('click', e => {
81
- var anchor, href;
82
- if ((anchor = e.target.closest('a')) && (href = anchor.href)
83
- // And not towards any target nor have a download directive
84
- && !anchor.target && !anchor.download && !href.includes('/my-account')
85
- // Same origin... but...
86
- && (!anchor.origin || anchor.origin === instance.location.origin)) {
87
- if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) {
88
- return;
89
- }
152
+ var anchor = e.target.closest('a');
153
+ if (!anchor || !anchor.href) return;
154
+ if (!anchor.target && !anchor.download && (!anchor.origin || anchor.origin === instance.location.origin)) {
155
+ if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return;
90
156
  // Publish everything, including hash
91
- Observer.set(instance.location, Url.copy(anchor), {
92
- detail: { type: 'link', src: anchor, },
93
- });
157
+ instance.go(Url.copy(anchor), { src: anchor, srcType: 'link', });
94
158
  // URLs with # will cause a natural navigation
95
159
  // even if pointing to a different page, a natural navigation will still happen
96
160
  // because with the Observer.set() above, window.document.location.href would have become
97
161
  // the destination page, which makes it look like same page navigation
98
- if (!href.includes('#')) {
162
+ if (!anchor.href.includes('#')) {
99
163
  e.preventDefault();
100
164
  }
101
165
  }
@@ -105,25 +169,27 @@ export default class Http {
105
169
  // Capture all form-submit
106
170
  // and fire to this router.
107
171
  window.addEventListener('submit', e => {
108
- var actionEl = window.document.createElement('a'),
109
- form = e.target.closest('form'),
172
+ var form = e.target.closest('form'),
110
173
  submits = [e.submitter]; //_arrFrom(form.elements).filter(el => el.matches('button,input[type="submit"],input[type="image"]'));
111
174
  var submitParams = [ 'action', 'enctype', 'method', 'noValidate', 'target' ].reduce((params, prop) => {
112
175
  params[prop] = submits.reduce((val, el) => val || (el.hasAttribute(`form${prop.toLowerCase()}`) ? el[`form${_toTitle(prop)}`] : null), null) || form[prop];
113
176
  return params;
114
177
  }, {});
115
178
  // We support method hacking
116
- // ---------------
117
179
  submitParams.method = e.submitter.dataset.method || form.dataset.method || submitParams.method;
118
180
  // ---------------
119
- if ((actionEl.href = submitParams.action) && !submitParams.target
120
- // Same origin... but...
121
- && (!actionEl.origin || actionEl.origin === instance.location.origin)) {
181
+ var actionEl = window.document.createElement('a');
182
+ actionEl.href = submitParams.action;
183
+ // ---------------
184
+ // If not targeted and same origin...
185
+ if (!submitParams.target && (!actionEl.origin || actionEl.origin === instance.location.origin)) {
186
+ if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return;
187
+ // Build data
122
188
  var formData = new FormData(form);
123
189
  if (e.submitter.name) {
124
190
  formData.set(e.submitter.name, e.submitter.value);
125
191
  }
126
- if (submitParams.method === 'get') {
192
+ if (submitParams.method.toUpperCase() === 'GET') {
127
193
  var query = wwwFormUnserialize(actionEl.search);
128
194
  Array.from(formData.entries()).forEach(_entry => {
129
195
  wwwFormSet(query, _entry[0], _entry[1], false);
@@ -131,13 +197,7 @@ export default class Http {
131
197
  actionEl.search = wwwFormSerialize(query);
132
198
  formData = null;
133
199
  }
134
- if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) {
135
- return;
136
- }
137
- // Publish everything, including hash
138
- Observer.set(instance.location, Url.copy(actionEl), {
139
- detail: { type: 'form', src: form, submitParams, data: formData },
140
- });
200
+ instance.go(Url.copy(actionEl), { ...submitParams, body: formData, src: form, srcType: 'form', });
141
201
  // URLs with # will cause a natural navigation
142
202
  // even if pointing to a different page, a natural navigation will still happen
143
203
  // because with the Observer.set() above, window.document.location.href would have become
@@ -148,71 +208,9 @@ export default class Http {
148
208
  }
149
209
  });
150
210
 
151
- /**
152
- * ----------------
153
- * instance.history
154
- * ----------------
155
- */
156
-
157
- instance.history = window.history;
158
211
  // -----------------------
159
- // Syndicate changes to
160
- // the browser;s location bar
161
- Observer.observe(instance.location, [[ 'href' ]], e => {
162
- e = e[0];
163
- if ((e.detail || {}).src === window.document.location) {
164
- // Already from a "popstate" event as above, so don't push again
165
- return;
166
- }
167
- if (e.value === 'http:') return;
168
- if (e.value === window.document.location.href || e.value + '/' === window.document.location.href) {
169
- instance.history.replaceState(instance.history.state, '', instance.location.href);
170
- } else {
171
- try {
172
- instance.history.pushState(instance.history.state, '', instance.location.href);
173
- } catch(e) {}
174
- }
175
- }, { diff: true });
176
-
177
- // ----------------------------------
178
- const createRequest = (url, referrer, e = {}) => {
179
- var detail = e.detail || {};
180
- var options = {
181
- method: (detail.submitParams || detail.src || {}).method || 'get',
182
- body: detail.data,
183
- headers: { ...(detail.headers || {}), 'X-Powered-By': '@webqit/webflo', },
184
- referrer,
185
- };
186
- return new StdRequest(url, options);
187
- };
188
- const handleResponse = response => {
189
- if (response && response.redirected) {
190
- window.location = response.url;
191
- return;
192
- var actionEl = window.document.createElement('a');
193
- if ((actionEl.href = response.url) && (!actionEl.origin || actionEl.origin === instance.location.origin)) {
194
- Observer.set(instance.location, { href: response.url }, {
195
- detail: { follow: false },
196
- });
197
- }
198
- }
199
- };
200
- // ----------------------------------
201
-
202
- // Observe location and route
203
- Observer.observe(instance.location, [['href']], async e => {
204
- e = e[0];
205
- var detail = e.detail || {};
206
- if (detail.follow === false) return;
207
- var method = (detail.submitParams || detail.src || {}).method;
208
- if ((_before(e.value, '#') !== _before(e.oldValue, '#')) || (method && method.toUpperCase() !== 'GET')) {
209
- return handleResponse(await client.call(instance, createRequest(e.value, e.oldValue, e), e));
210
- }
211
- }, {diff: false /* method might be the difference */});
212
212
  // Startup route
213
-
214
- handleResponse(await client.call(instance, createRequest(window.document.location.href, document.referrer)));
215
-
213
+ instance.go(window.document.location.href, { referrer: document.referrer });
216
214
  return instance;
217
215
  }
218
216
 
@@ -74,50 +74,39 @@ export default function(layout, params) {
74
74
  // -----------------
75
75
  var networkProgress = networkProgressOngoing = new RequestHandle();
76
76
  networkProgress.setActive(true, event.request._method || event.request.method);
77
- // -----------------
78
- const headers = event.request.headers;
79
- if (!headers.get('Accept')) {
80
- headers.set('Accept', 'application/json');
81
- }
82
- if (!headers.get('Cache-Control')) {
83
- headers.set('Cache-Control', 'no-store');
84
- }
85
- // -----------------
86
- // Sync session data to cache to be available to service-worker routers
87
- const response = fetch(event.request, {}, networkProgress.updateProgress.bind(networkProgress));
88
- // -----------------
89
- // -----------------
90
- response.catch(e => networkProgress.throw(e.message));
91
- return response.then(async _response => {
92
- _response = new clientNavigationEvent.Response(_response.body, {
93
- status: _response.status,
94
- statusText: _response.statusText,
95
- headers: _response.headers,
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,
96
86
  _proxy: {
97
- url: _response.url,
98
- ok: _response.ok,
99
- redirected: _response.redirected
87
+ url: response.url,
88
+ ok: response.ok,
89
+ redirected: response.redirected
100
90
  },
101
91
  });
102
- // Save a reference to this
103
- $context.responseClone = _response;
104
- // Return a promise that never resolves as a new response is underway
105
- if (!networkProgress.active) {
106
- return new Promise(() => {});
92
+ if (response.headers.get('Location')) {
93
+ networkProgress.redirecting(response.headers.get('Location'));
107
94
  }
108
95
  // Stop loading status
109
96
  networkProgress.setActive(false);
110
- return _response;
97
+ return response;
111
98
  });
112
99
  });
113
100
  if ($context.response instanceof clientNavigationEvent.Response) {
114
- $context.response = await $context.response.data();
115
- }
101
+ $context.data = await $context.response.data();
102
+ } else {
103
+ $context.data = $context.response;
104
+ }
116
105
 
117
106
  // --------
118
107
  // Render
119
108
  // --------
120
- const rendering = await router.route('render', clientNavigationEvent, $context.response, async function(event, data) {
109
+ const rendering = await router.route('render', clientNavigationEvent, $context.data, async function(event, data) {
121
110
  // --------
122
111
  // OOHTML would waiting for DOM-ready in order to be initialized
123
112
  await new Promise(res => window.WebQit.DOM.ready(res));
@@ -168,7 +157,7 @@ export default function(layout, params) {
168
157
 
169
158
  }
170
159
 
171
- return $context.responseClone;
160
+ return $context.response;
172
161
  });
173
162
 
174
163
  };
@@ -202,6 +191,9 @@ class RequestHandle {
202
191
  valuetotal,
203
192
  });
204
193
  }
194
+ redirecting(location) {
195
+ Observer.set(networkWatch, 'redirecting', location);
196
+ }
205
197
  throw(message) {
206
198
  if (this.active === false) {
207
199
  return;
@@ -117,7 +117,7 @@ export default function(layout, params) {
117
117
  }
118
118
  return _response;
119
119
  }
120
-
120
+
121
121
  return defaultFetch(evt);
122
122
  };
123
123
  evt.respondWith(handleFetch(evt));
@@ -144,13 +144,12 @@ export default function(layout, params) {
144
144
  return network_fetch(evt);
145
145
  };
146
146
 
147
- //evt.request.mode navigate evt.request.cache force-cache evt.request.destination document request.headers.get('Accept') text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
148
147
  //evt.request.mode navigate evt.request.cache force-cache evt.request.destination document request.headers.get('Accept') text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
149
148
 
150
149
  const getCacheName = request => request.headers.get('Accept') === 'application/json'
151
150
  ? params.cache_name + '_json'
152
151
  : params.cache_name;
153
-
152
+
154
153
  // Caching strategy: cache_first
155
154
  const cache_fetch = (evt, cacheRefresh = false, is_Navigate_ForceCache_Document = false) => {
156
155
 
@@ -162,10 +161,10 @@ export default function(layout, params) {
162
161
  let url = new URL(request.url);
163
162
  url.searchParams.set('$force-cache', '1');
164
163
  request = new Request(url, {
165
- method: request.method,
164
+ method: request.method,
166
165
  headers: request.headers,
167
166
  body: request.body,
168
- mode: request.mode === 'navigate' ? null : request.mode,
167
+ mode: request.mode === 'navigate'/* throws */ ? null : request.mode,
169
168
  credentials: request.credentials,
170
169
  cache: request.cache,
171
170
  redirect: request.redirect,
@@ -17,7 +17,6 @@ import _isArray from '@webqit/util/js/isArray.js';
17
17
  import { _isString, _isPlainObject, _isPlainArray } from '@webqit/util/js/index.js';
18
18
  import _delay from '@webqit/util/js/delay.js';
19
19
  import { slice as _streamSlice } from 'stream-slice';
20
- import { v4 as uuidv4, v5 as uuidv5 } from 'uuid';
21
20
  import * as config from '../../config/index.js';
22
21
  import * as services from '../../services/index.js';
23
22
  import NavigationEvent from './NavigationEvent.js';
@@ -408,12 +407,7 @@ export async function run(hostSetup, request, response, Ui, flags = {}, protocol
408
407
  // -------------------
409
408
  // Chrome needs this for audio elements to play
410
409
  response.setHeader('Accept-Ranges', 'bytes');
411
- /*
412
- if ($context.response.headers.contentLength && !$context.response.headers.contentRange) {
413
- $context.response.headers.contentRange = `bytes 0-${$context.response.headers.contentLength}/${$context.response.headers.contentLength}`;
414
- }
415
410
 
416
- */
417
411
  // -------------------
418
412
  // Automatic response headers
419
413
  // -------------------
@@ -470,8 +464,15 @@ export async function run(hostSetup, request, response, Ui, flags = {}, protocol
470
464
  // Send
471
465
  // -------------------
472
466
  if ($context.response.headers.redirect) {
473
- response.statusCode = $context.response.status;
474
- response.end();
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();
472
+ } else {
473
+ response.statusCode = $context.response.status;
474
+ response.end();
475
+ }
475
476
  } else if ($context.response.original !== undefined && $context.response.original !== null) {
476
477
  response.statusCode = $context.response.status;
477
478
  response.statusMessage = $context.response.statusText;
@@ -551,15 +552,23 @@ export async function run(hostSetup, request, response, Ui, flags = {}, protocol
551
552
  // --------
552
553
 
553
554
  if (flags.logs !== false) {
555
+ 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;
554
559
  Ui.log(''
555
560
  + '[' + (hostSetup.vh ? Ui.style.keyword(hostSetup.vh.host) + '][' : '') + Ui.style.comment((new Date).toUTCString()) + '] '
556
561
  + Ui.style.keyword(protocol.toUpperCase() + ' ' + serverNavigationEvent.request.method) + ' '
557
562
  + Ui.style.url(serverNavigationEvent.request.url) + ($context.response && ($context.response.meta || {}).autoIndex ? Ui.style.comment((!serverNavigationEvent.request.url.endsWith('/') ? '/' : '') + $context.response.meta.autoIndex) : '') + ' '
558
563
  + (' (' + Ui.style.comment($context.response && ($context.response.headers || {}).contentType ? $context.response.headers.contentType : 'unknown') + ') ')
559
564
  + (
560
- [ 404, 500 ].includes(response.statusCode)
561
- ? Ui.style.err(response.statusCode + ($context.fatal ? ` [ERROR]: ${$context.fatal.error || $context.fatal.toString()}` : ``))
562
- : Ui.style.val(response.statusCode) + ((response.statusCode + '').startsWith('3') ? ' - ' + Ui.style.val(response.getHeader('Location')) : ' (' + Ui.style.keyword(response.getHeader('Content-Range') || response.statusMessage) + ')')
565
+ errorCode
566
+ ? Ui.style.err(errorCode + ($context.fatal ? ` [ERROR]: ${$context.fatal.error || $context.fatal.toString()}` : ``))
567
+ : Ui.style.val(statusCode) + (
568
+ redirectCode
569
+ ? ' - ' + Ui.style.val(response.getHeader('Location'))
570
+ : ' (' + Ui.style.keyword(response.getHeader('Content-Range') || response.statusMessage) + ')'
571
+ )
563
572
  )
564
573
  );
565
574
  }