@webqit/webflo 0.8.76 → 0.9.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 (97) hide show
  1. package/package.json +5 -12
  2. package/src/Cli.js +131 -0
  3. package/src/Configurator.js +97 -0
  4. package/src/Context.js +76 -0
  5. package/src/config-pi/deployment/Env.js +69 -0
  6. package/src/config-pi/deployment/Layout.js +65 -0
  7. package/src/config-pi/deployment/Origins.js +133 -0
  8. package/src/config-pi/deployment/Virtualization.js +65 -0
  9. package/src/config-pi/deployment/index.js +18 -0
  10. package/src/config-pi/index.js +16 -0
  11. package/src/config-pi/runtime/Client.js +59 -0
  12. package/src/config-pi/runtime/Server.js +174 -0
  13. package/src/config-pi/runtime/client/Worker.js +117 -0
  14. package/src/config-pi/runtime/client/index.js +12 -0
  15. package/src/config-pi/runtime/index.js +18 -0
  16. package/src/config-pi/runtime/server/Headers.js +90 -0
  17. package/src/config-pi/runtime/server/Redirects.js +108 -0
  18. package/src/config-pi/runtime/server/index.js +14 -0
  19. package/src/config-pi/static/Manifest.js +321 -0
  20. package/src/config-pi/static/Ssg.js +72 -0
  21. package/src/config-pi/static/index.js +14 -0
  22. package/src/deployment-pi/index.js +10 -0
  23. package/src/{services → deployment-pi}/origins/index.js +88 -58
  24. package/src/index.js +14 -147
  25. package/src/{runtime → runtime-pi}/Router.js +19 -19
  26. package/src/runtime-pi/client/Context.js +7 -0
  27. package/src/{runtime → runtime-pi}/client/Router.js +2 -2
  28. package/src/{runtime/client/Navigator.js → runtime-pi/client/Runtime.js} +143 -102
  29. package/src/runtime-pi/client/RuntimeClient.js +114 -0
  30. package/src/{runtime → runtime-pi}/client/Storage.js +8 -7
  31. package/src/{runtime → runtime-pi}/client/Url.js +2 -6
  32. package/src/{runtime/client/WorkerClient.js → runtime-pi/client/WorkerComm.js} +2 -2
  33. package/src/runtime-pi/client/generate.js +242 -0
  34. package/src/runtime-pi/client/generate.oohtml.js +7 -0
  35. package/src/runtime-pi/client/index.js +18 -0
  36. package/src/runtime-pi/client/whatwag.js +27 -0
  37. package/src/runtime-pi/client/worker/Context.js +7 -0
  38. package/src/runtime-pi/client/worker/Worker.js +243 -0
  39. package/src/runtime-pi/client/worker/WorkerClient.js +46 -0
  40. package/src/runtime-pi/client/worker/index.js +18 -0
  41. package/src/runtime-pi/index.js +14 -0
  42. package/src/runtime-pi/server/Context.js +16 -0
  43. package/src/{runtime → runtime-pi}/server/Router.js +6 -6
  44. package/src/runtime-pi/server/Runtime.js +531 -0
  45. package/src/runtime-pi/server/RuntimeClient.js +102 -0
  46. package/src/runtime-pi/server/index.js +41 -0
  47. package/src/runtime-pi/server/whatwag.js +35 -0
  48. package/src/{runtime → runtime-pi}/util.js +0 -0
  49. package/src/{runtime/_FormData.js → runtime-pi/xFormData.js} +2 -2
  50. package/src/{runtime/_Headers.js → runtime-pi/xHeaders.js} +4 -4
  51. package/src/runtime-pi/xHttpEvent.js +93 -0
  52. package/src/runtime-pi/xHttpMessage.js +179 -0
  53. package/src/runtime-pi/xRequest.js +67 -0
  54. package/src/runtime-pi/xRequestHeaders.js +95 -0
  55. package/src/runtime-pi/xResponse.js +62 -0
  56. package/src/{runtime/_ResponseHeaders.js → runtime-pi/xResponseHeaders.js} +38 -18
  57. package/src/{runtime/_URL.js → runtime-pi/xURL.js} +4 -4
  58. package/src/runtime-pi/xfetch.js +7 -0
  59. package/src/{services → services-pi}/certbot/http-auth-hook.js +0 -0
  60. package/src/{services → services-pi}/certbot/http-cleanup-hook.js +0 -0
  61. package/src/{services → services-pi}/certbot/index.js +21 -15
  62. package/src/services-pi/index.js +9 -0
  63. package/src/static-pi/index.js +11 -0
  64. package/src/webflo.js +33 -0
  65. package/test/index.test.js +26 -0
  66. package/src/build/client/index.js +0 -261
  67. package/src/build/index.js +0 -5
  68. package/src/config/client.js +0 -191
  69. package/src/config/headers.js +0 -121
  70. package/src/config/index.js +0 -14
  71. package/src/config/layout.js +0 -83
  72. package/src/config/manifest.js +0 -341
  73. package/src/config/origins.js +0 -165
  74. package/src/config/prerendering.js +0 -100
  75. package/src/config/redirects.js +0 -137
  76. package/src/config/server.js +0 -201
  77. package/src/config/variables.js +0 -102
  78. package/src/config/vhosts.js +0 -93
  79. package/src/runtime/_MessageStream.js +0 -195
  80. package/src/runtime/_NavigationEvent.js +0 -91
  81. package/src/runtime/_Request.js +0 -61
  82. package/src/runtime/_RequestHeaders.js +0 -72
  83. package/src/runtime/_Response.js +0 -56
  84. package/src/runtime/client/Cache.js +0 -38
  85. package/src/runtime/client/Http.js +0 -225
  86. package/src/runtime/client/NavigationEvent.js +0 -21
  87. package/src/runtime/client/Runtime.js +0 -126
  88. package/src/runtime/client/StdRequest.js +0 -74
  89. package/src/runtime/client/Worker.js +0 -312
  90. package/src/runtime/client/WorkerComm.js +0 -183
  91. package/src/runtime/client/effects/sounds.js +0 -64
  92. package/src/runtime/index.js +0 -5
  93. package/src/runtime/server/NavigationEvent.js +0 -39
  94. package/src/runtime/server/Runtime.js +0 -593
  95. package/src/runtime/server/index.js +0 -183
  96. package/src/runtime/server/index.mjs +0 -10
  97. package/src/services/index.js +0 -6
@@ -0,0 +1,14 @@
1
+
2
+ /**
3
+ * @imports
4
+ */
5
+ import * as server from './server/index.js';
6
+ import * as client from './client/generate.js';
7
+
8
+ /**
9
+ * @exports
10
+ */
11
+ export {
12
+ server,
13
+ client,
14
+ }
@@ -0,0 +1,16 @@
1
+
2
+ /**
3
+ * @imports
4
+ */
5
+ import _Contex from '../../Context.js';
6
+
7
+ export default class Context extends _Contex {
8
+ // env
9
+ get env() {
10
+ return this.dict.env || {};
11
+ }
12
+
13
+ set env(value) {
14
+ this.dict.env = value;
15
+ }
16
+ }
@@ -23,8 +23,8 @@ export default class Router extends _Router {
23
23
  if (_segmentOnFile.index) return _segmentOnFile;
24
24
  var _currentPath = thisTick.trailOnFile.concat(_seg).join('/'),
25
25
  routeHandlerFile;
26
- return Fs.existsSync(routeHandlerFile = Path.join(this.layout.ROOT, this.layout.SERVER_DIR, _currentPath, 'index.js')) ? { seg: _seg, index: routeHandlerFile } : (
27
- Fs.existsSync(Path.join(this.layout.ROOT, this.layout.SERVER_DIR, _currentPath)) ? { seg: _seg, dirExists: true } : _segmentOnFile
26
+ return Fs.existsSync(routeHandlerFile = Path.join(this.cx.CWD, this.cx.layout.SERVER_DIR, _currentPath, 'index.js')) ? { seg: _seg, index: routeHandlerFile } : (
27
+ Fs.existsSync(Path.join(this.cx.CWD, this.cx.layout.SERVER_DIR, _currentPath)) ? { seg: _seg, dirExists: true } : _segmentOnFile
28
28
  );
29
29
  }, { seg: null });
30
30
  thisTick.trail.push(thisTick.currentSegment);
@@ -33,7 +33,7 @@ export default class Router extends _Router {
33
33
  } else {
34
34
  thisTick.trail = [];
35
35
  thisTick.trailOnFile = [];
36
- thisTick.currentSegmentOnFile = { index: Path.join(this.layout.ROOT, this.layout.SERVER_DIR, 'index.js') };
36
+ thisTick.currentSegmentOnFile = { index: Path.join(this.cx.CWD, this.cx.layout.SERVER_DIR, 'index.js') };
37
37
  thisTick.exports = Fs.existsSync(thisTick.currentSegmentOnFile.index)
38
38
  ? await import(Url.pathToFileURL(thisTick.currentSegmentOnFile.index))
39
39
  : null;
@@ -58,9 +58,9 @@ export default class Router extends _Router {
58
58
  *
59
59
  * @return Promise
60
60
  */
61
- fetch(event) {
61
+ file(event) {
62
62
  var filename = event.url.pathname;
63
- var _filename = Path.join(this.layout.ROOT, this.layout.PUBLIC_DIR, decodeURIComponent(filename));
63
+ var _filename = Path.join(this.cx.CWD, this.cx.layout.PUBLIC_DIR, decodeURIComponent(filename));
64
64
  var autoIndex;
65
65
  if (Fs.existsSync(_filename)) {
66
66
  // based on the URL path, extract the file extention. e.g. .js, .doc, ...
@@ -115,7 +115,7 @@ export default class Router extends _Router {
115
115
  * @return bool
116
116
  */
117
117
  putPreRendered(filename, content) {
118
- var _filename = Path.join(this.layout.PUBLIC_DIR, '.', filename);
118
+ var _filename = Path.join(this.cx.layout.PUBLIC_DIR, '.', filename);
119
119
  if (!Path.parse(filename).ext && filename.lastIndexOf('.') < filename.lastIndexOf('/')) {
120
120
  _filename = Path.join(_filename, '/index.html');
121
121
  }
@@ -0,0 +1,531 @@
1
+
2
+ /**
3
+ * @imports
4
+ */
5
+ import Fs from 'fs';
6
+ import Path from 'path';
7
+ import Http from 'http';
8
+ import Https from 'https';
9
+ import Formidable from 'formidable';
10
+ import Sessions from 'client-sessions';
11
+ import { Observer } from '@webqit/oohtml-ssr/apis.js';
12
+ import { _each } from '@webqit/util/obj/index.js';
13
+ import { _isEmpty } from '@webqit/util/js/index.js';
14
+ import { _from as _arrFrom } from '@webqit/util/arr/index.js';
15
+ import { slice as _streamSlice } from 'stream-slice';
16
+ import * as whatwag from './whatwag.js';
17
+ import xURL from '../xURL.js';
18
+ import xFormData from "../xFormData.js";
19
+ import xRequestHeaders from "../xRequestHeaders.js";
20
+ import xResponseHeaders from "../xResponseHeaders.js";
21
+ import xRequest from "../xRequest.js";
22
+ import xResponse from "../xResponse.js";
23
+ import xfetch from '../xfetch.js';
24
+ import xHttpEvent from '../xHttpEvent.js';
25
+
26
+ const URL = xURL(whatwag.URL);
27
+ const FormData = xFormData(whatwag.FormData);
28
+ const ReadableStream = whatwag.ReadableStream;
29
+ const RequestHeaders = xRequestHeaders(whatwag.Headers);
30
+ const ResponseHeaders = xResponseHeaders(whatwag.Headers);
31
+ const Request = xRequest(whatwag.Request, RequestHeaders, FormData);
32
+ const Response = xResponse(whatwag.Response, ResponseHeaders, FormData);
33
+ const fetch = xfetch(whatwag.fetch);
34
+ const HttpEvent = xHttpEvent(Request, Response, URL);
35
+
36
+ export {
37
+ URL,
38
+ FormData,
39
+ ReadableStream,
40
+ RequestHeaders,
41
+ ResponseHeaders,
42
+ Request,
43
+ Response,
44
+ fetch,
45
+ HttpEvent,
46
+ Observer,
47
+ }
48
+
49
+ export default class Runtime {
50
+
51
+ /**
52
+ * Runtime
53
+ *
54
+ * @param Object cx
55
+ * @param Function clientCallback
56
+ *
57
+ * @return void
58
+ */
59
+ constructor(cx, clientCallback) {
60
+ // ---------------
61
+ this.cx = cx;
62
+ this.clients = new Map;
63
+ this.mockSessionStore = {};
64
+ this.ready = (async () => {
65
+
66
+ // ---------------
67
+
68
+ const execClientCallback = (cx, hostname) => {
69
+ let client = clientCallback(cx, hostname);
70
+ if (!client || !client.handle) throw new Error(`Application instance must define a ".handle()" method.`);
71
+ return client;
72
+ };
73
+ const loadContextObj = async cx => {
74
+ const meta = {};
75
+ if (_isEmpty(cx.server)) { cx.server = await (new this.cx.config.runtime.Server(cx)).read(); }
76
+ if (_isEmpty(cx.layout)) { cx.layout = await (new this.cx.config.deployment.Layout(cx)).read(); }
77
+ if (_isEmpty(cx.env)) {
78
+ let env = await (new this.cx.config.deployment.Env(cx)).read();
79
+ cx.env = env.entries;
80
+ meta.envAutoloading = env.autoload;
81
+ }
82
+ return meta;
83
+ };
84
+ let loadMeta = await loadContextObj(this.cx);
85
+ if (this.cx.server.shared && (this.cx.config.deployment.Virtualization || !_isEmpty(this.cx.vcontexts))) {
86
+ if (_isEmpty(this.cx.vcontexts)) {
87
+ this.cx.vcontexts = {};
88
+ const vhosts = await (new this.cx.config.deployment.Virtualization(cx)).read();
89
+ await Promise.all((vhosts.entries || []).map(vhost => async () => {
90
+ this.cx.vcontexts[vhost.host] = this.cx.constructor.create(this.cx, Path.join(this.cx.CWD, vhost.path));
91
+ await loadContextObj(this.cx.vcontexts[vhost.host]);
92
+ }));
93
+ }
94
+ _each(this.cx.vcontexts, (host, vcontext) => {
95
+ this.clients.set(vhost.host, execClientCallback(vcontext, host));
96
+ });
97
+ } else {
98
+ this.clients.set('*', execClientCallback(this.cx, '*'));
99
+ }
100
+ // Always populate... regardless whether shared setup
101
+ if (loadMeta.envAutoloading !== false) {
102
+ Object.keys(this.cx.env).forEach(key => {
103
+ if (!(key in process.env)) {
104
+ process.env[key] = this.cx.env[key];
105
+ }
106
+ });
107
+ }
108
+
109
+ // ---------------
110
+
111
+ if (!this.cx.flags['test-only'] && !this.cx.flags['https-only']) {
112
+ Http.createServer((request, response) => handleRequest('http', request, response)).listen(process.env.PORT || this.cx.server.port);
113
+ }
114
+
115
+ // ---------------
116
+
117
+ if (!this.cx.flags['test-only'] && !this.cx.flags['http-only'] && this.cx.server.https.port) {
118
+ const httpsServer = Https.createServer((request, response) => handleRequest('https', request, response));
119
+ if (this.cx.server.shared) {
120
+ _each(this.cx.vcontexts, (host, vcontext) => {
121
+ if (Fs.existsSync(vcontext.server.https.keyfile)) {
122
+ const cert = {
123
+ key: Fs.readFileSync(vcontext.server.https.keyfile),
124
+ cert: Fs.readFileSync(vcontext.server.https.certfile),
125
+ };
126
+ var domains = _arrFrom(vcontext.server.https.certdoms);
127
+ if (!domains[0] || domains[0].trim() === '*') {
128
+ httpsServer.addContext(host, cert);
129
+ if (vcontext.server.force_www) {
130
+ httpsServer.addContext(host.startsWith('www.') ? host.substr(4) : 'www.' + host, cert);
131
+ }
132
+ } else {
133
+ domains.forEach(domain => {
134
+ httpsServer.addContext(domain, cert);
135
+ });
136
+ }
137
+ }
138
+ });
139
+ } else {
140
+ if (Fs.existsSync(this.cx.server.https.keyfile)) {
141
+ var domains = _arrFrom(this.cx.server.https.certdoms);
142
+ var cert = {
143
+ key: Fs.readFileSync(this.cx.server.https.keyfile),
144
+ cert: Fs.readFileSync(this.cx.server.https.certfile),
145
+ };
146
+ if (!domains[0]) {
147
+ domains = ['*'];
148
+ }
149
+ domains.forEach(domain => {
150
+ httpsServer.addContext(domain, cert);
151
+ });
152
+ }
153
+ }
154
+ httpsServer.listen(process.env.PORT2 || this.cx.server.https.port);
155
+ }
156
+
157
+ // ---------------
158
+
159
+ const handleRequest = async (protocol, request, response) => {
160
+ // --------
161
+ // Parse request
162
+ const fullUrl = protocol + '://' + request.headers.host + request.url;
163
+ const requestInit = { method: request.method, headers: request.headers };
164
+ if (request.method !== 'GET' && request.method !== 'HEAD') {
165
+ requestInit.body = await new Promise((resolve, reject) => {
166
+ var formidable = new Formidable.IncomingForm({ multiples: true, allowEmptyFiles: false, keepExtensions: true });
167
+ formidable.parse(request, (error, fields, files) => {
168
+ if (error) {
169
+ reject(error);
170
+ return;
171
+ }
172
+ if (request.headers['content-type'] === 'application/json') {
173
+ return resolve(fields);
174
+ }
175
+ const formData = new FormData;
176
+ Object.keys(fields).forEach(name => {
177
+ if (Array.isArray(fields[name])) {
178
+ const values = Array.isArray(fields[name][0])
179
+ ? fields[name][0]/* bugly a nested array when there are actually more than entry */
180
+ : fields[name];
181
+ values.forEach(value => {
182
+ formData.append(!name.endsWith(']') ? name + '[]' : name, value);
183
+ });
184
+ } else {
185
+ formData.append(name, fields[name]);
186
+ }
187
+ });
188
+ Object.keys(files).forEach(name => {
189
+ const fileCompat = file => {
190
+ // IMPORTANT
191
+ // Path up the "formidable" file in a way that "formdata-node"
192
+ // to can translate it into its own file instance
193
+ file[Symbol.toStringTag] = 'File';
194
+ file.stream = () => Fs.createReadStream(file.path);
195
+ // Done pathcing
196
+ return file;
197
+ }
198
+ if (Array.isArray(files[name])) {
199
+ files[name].forEach(value => {
200
+ formData.append(name, fileCompat(value));
201
+ });
202
+ } else {
203
+ formData.append(name, fileCompat(files[name]));
204
+ }
205
+ });
206
+ resolve(formData);
207
+ });
208
+ });
209
+ }
210
+
211
+ // --------
212
+ // Run Application
213
+ let clientResponse = await this.go(fullUrl, requestInit, { request, response });
214
+ if (response.headersSent) return;
215
+
216
+ // --------
217
+ // Set headers
218
+ _each(clientResponse.headers.json(), (name, value) => {
219
+ response.setHeader(name, value);
220
+ });
221
+
222
+ // --------
223
+ // Send
224
+ response.statusCode = clientResponse.status;
225
+ response.statusMessage = clientResponse.statusText;
226
+ if (clientResponse.headers.redirect) {
227
+ response.end();
228
+ } else {
229
+ var body = clientResponse.body;
230
+ if ((body instanceof ReadableStream)) {
231
+ body.pipe(response);
232
+ } else {
233
+ // The default
234
+ if (clientResponse.headers.contentType === 'application/json') {
235
+ body += '';
236
+ }
237
+ response.end(body);
238
+ }
239
+ }
240
+ };
241
+
242
+ })();
243
+ // ---------------
244
+ Observer.set(this, 'location', {});
245
+ Observer.set(this, 'network', {});
246
+ // ---------------
247
+ if (this.cx.app.title && this.cx.logger) {
248
+ this.cx.logger.info(`> Server running (${this.cx.app.title || ''})`);
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Performs a request.
254
+ *
255
+ * @param object|string url
256
+ * @param object init
257
+ * @param object detail
258
+ *
259
+ * @return Response
260
+ */
261
+ async go(url, init = {}, detail = {}) {
262
+ await this.ready;
263
+
264
+ // ------------
265
+ url = typeof url === 'string' ? new whatwag.URL(url) : url;
266
+ init = { referrer: this.location.href, ...init };
267
+ // ------------
268
+ let _context = this.cx, rdr;
269
+ if (this.cx.server.shared && !(_context = this.cx.vcontexts[url.hostname])
270
+ && !(url.hostname.startsWith('www.') && (_context = this.cx.vcontexts[url.hostname.substr(4)]) && _context.server.force_www)
271
+ && !(!url.hostname.startsWith('www.') && (_context = this.cx.vcontexts['www.' + url.hostname]) && _context.server.force_www)) {
272
+ rdr = { status: 500, statusText: 'Unrecognized host' };
273
+ } else if (url.protocol === 'http:' && _context.server.https.force && !this.cx.flags['http-only'] && /** main server */this.cx.server.https.port) {
274
+ rdr = { status: 302, headers: { Location: ( url.protocol = 'https:', url.href ) } };
275
+ } else if (url.hostname.startsWith('www.') && _context.server.force_www === 'remove') {
276
+ rdr = { status: 302, headers: { Location: ( url.hostname = url.hostname.substr(4), url.href ) } };
277
+ } else if (!url.hostname.startsWith('www.') && _context.server.force_www === 'add') {
278
+ rdr = { status: 302, headers: { Location: ( url.hostname = `www.${url.hostname}`, url.href ) } };
279
+ } else if (_context.config.Redirects && (rdr = await (new _context.config.Redirects(_context)).match(url.href))) {
280
+ rdr = { status: rdr.code || 301 /* Permanent */, headers: { Location: rdr.target } };
281
+ }
282
+ if (rdr) {
283
+ return Response(null, rdr);
284
+ }
285
+ const autoHeaders = _context.config.Headers
286
+ ? await (new _context.config.Headers(_context)).match(url.href)
287
+ : [];
288
+ // ------------
289
+
290
+ // ------------
291
+ Observer.set(this.location, url, { detail: { ...init, ...detail, } });
292
+ Observer.set(this.network, 'redirecting', null);
293
+ // ------------
294
+
295
+ // The request object
296
+ let request = this.generateRequest(url.href, init, autoHeaders.filter(header => header.type === 'request'));
297
+ // The navigation event
298
+ let httpEvent = new HttpEvent(request, detail, (id = 'session', options = { duration: 60 * 60 * 24, activeDuration: 60 * 60 * 24 }, callback = null) => {
299
+ return this.getSession(_context, httpEvent, id, options, callback);
300
+ });
301
+ // Response
302
+ let client = this.clients.get('*');
303
+ if (this.cx.server.shared) {
304
+ client = this.clients.get(url.hostname);
305
+ }
306
+ let response = await client.handle(httpEvent, (...args) => this.remoteFetch(...args));
307
+ let finalResponse = await this.handleResponse(httpEvent, response, autoHeaders.filter(header => header.type === 'response'));
308
+ // Logging
309
+ if (this.cx.logger) {
310
+ let log = this.generateLog(httpEvent, finalResponse);
311
+ this.cx.logger.log(log);
312
+ }
313
+ // Return value
314
+ return finalResponse;
315
+ }
316
+
317
+ // Generates request object
318
+ generateRequest(href, init, autoHeaders = []) {
319
+ let request = new Request(href, init);
320
+ this._autoHeaders(request.headers, autoHeaders);
321
+ return request;
322
+ }
323
+
324
+ // Generates session object
325
+ getSession(cx, e, id, options = {}, callback = null) {
326
+ let baseObject;
327
+ if (!(e.detail.request && e.detail.response)) {
328
+ baseObject = this.mockSessionStore;
329
+ let cookieAvailability = e.request.headers.cookies[id]; // We just want to know availability... not validity, as this is understood to be for testing purposes only
330
+ if (!(this.mockSessionStore[id] && cookieAvailability)) {
331
+ let cookieObj = {};
332
+ Object.defineProperty(this.mockSessionStore, id, {
333
+ get: () => cookieObj,
334
+ set: value => (cookieObj = value),
335
+ });
336
+ }
337
+ } else {
338
+ baseObject = e.detail.request;
339
+ Sessions({
340
+ duration: 0, // how long the session will stay valid in ms
341
+ activeDuration: 0, // if expiresIn < activeDuration, the session will be extended by activeDuration milliseconds
342
+ ...options,
343
+ cookieName: id, // cookie name dictates the key name added to the request object
344
+ secret: cx.env.SESSION_KEY, // should be a large unguessable string
345
+ })(e.detail.request, e.detail.response, e => {
346
+ if (e) {
347
+ if (!callback) throw e;
348
+ callback(e);
349
+ }
350
+ });
351
+ }
352
+ // Where theres no error, instance is available
353
+ let instance = Object.getOwnPropertyDescriptor(baseObject, id);
354
+ if (!callback) return instance;
355
+ if (instance) callback(null, instance);
356
+ }
357
+
358
+ // Initiates remote fetch and sets the status
359
+ remoteFetch(request) {
360
+ Observer.set(this.network, 'remote', true);
361
+ let _response = fetch(request);
362
+ // This catch() is NOT intended to handle failure of the fetch
363
+ _response.catch(e => Observer.set(this.network, 'error', e.message));
364
+ // Save a reference to this
365
+ return _response.then(async response => {
366
+ // Stop loading status
367
+ Observer.set(this.network, 'remote', false);
368
+ return new Response(response);
369
+ });
370
+ }
371
+
372
+ // Handles response object
373
+ async handleResponse(e, response, autoHeaders = []) {
374
+ if (!(response instanceof Response)) { response = new Response(response); }
375
+ Observer.set(this.network, 'remote', false);
376
+ Observer.set(this.network, 'error', null);
377
+
378
+ // ----------------
379
+ // Mock-Cookies?
380
+ // ----------------
381
+ if (!(e.detail.request && e.detail.response)) {
382
+ for (let cookieName of Object.getOwnPropertyNames(this.mockSessionStore)) {
383
+ response.headers.set('Set-Cookie', `${cookieName}=1`); // We just want to know availability... not validity, as this is understood to be for testing purposes only
384
+ }
385
+ }
386
+
387
+ // ----------------
388
+ // Auto-Headers
389
+ // ----------------
390
+ response.headers.set('Accept-Ranges', 'bytes');
391
+ this._autoHeaders(response.headers, autoHeaders);
392
+
393
+ // ----------------
394
+ // Redirects
395
+ // ----------------
396
+ if (response.headers.redirect) {
397
+ let xRedirectPolicy = e.request.headers.get('X-Redirect-Policy');
398
+ let xRedirectCode = e.request.headers.get('X-Redirect-Code') || 300;
399
+ let isSameOriginRedirect = (new whatwag.URL(response.headers.location, e.url.origin)).origin === e.url.origin;
400
+ if (xRedirectPolicy === 'manual' || (!isSameOriginRedirect && xRedirectPolicy === 'manual-when-cross-origin') || (isSameOriginRedirect && xRedirectPolicy === 'manual-when-same-origin')) {
401
+ response.attrs.status = xRedirectCode;
402
+ response.headers.json({
403
+ 'X-Redirect-Code': response.status,
404
+ 'Cache-Control': 'no-store',
405
+ });
406
+ }
407
+ return response;
408
+ }
409
+
410
+ // ----------------
411
+ // 404
412
+ // ----------------
413
+ if (response.bodyAttrs.input === undefined || response.bodyAttrs.input === null) {
414
+ response.attrs.status = response.status !== 200 ? response.status : 404;
415
+ response.attrs.statusText = `${e.request.url} not found!`;
416
+ return response;
417
+ }
418
+
419
+ // ----------------
420
+ // Not acceptable
421
+ // ----------------
422
+ if (e.request.headers.get('Accept') && !e.request.headers.accept.match(response.headers.contentType)) {
423
+ response.attrs.status = 406;
424
+ return response;
425
+ }
426
+
427
+ // ----------------
428
+ // Important no-caching
429
+ // for non-"get" requests
430
+ // ----------------
431
+ if (e.request.method !== 'GET' && !response.headers.get('Cache-Control')) {
432
+ response.headers.set('Cache-Control', 'no-store');
433
+ }
434
+
435
+ // ----------------
436
+ // Body
437
+ // ----------------
438
+ let rangeRequest, body = response.body;
439
+ if ((rangeRequest = e.request.headers.range) && !response.headers.get('Content-Range')
440
+ && ((body instanceof ReadableStream) || (ArrayBuffer.isView(body) && (body = ReadableStream.from(body))))) {
441
+ // ...in partials
442
+ let totalLength = response.headers.contentLength || 0;
443
+ let ranges = await rangeRequest.reduce(async (_ranges, range) => {
444
+ _ranges = await _ranges;
445
+ if (range[0] < 0 || (totalLength && range[0] > totalLength)
446
+ || (range[1] > -1 && (range[1] <= range[0] || (totalLength && range[1] >= totalLength)))) {
447
+ // The range is beyond upper/lower limits
448
+ _ranges.error = true;
449
+ }
450
+ if (!totalLength && range[0] === undefined) {
451
+ // totalLength is unknown and we cant read the trailing size specified in range[1]
452
+ _ranges.error = true;
453
+ }
454
+ if (_ranges.error) return _ranges;
455
+ if (totalLength) { range.clamp(totalLength); }
456
+ let partLength = range[1] - range[0] + 1;
457
+ _ranges.parts.push({
458
+ body: body.pipe(_streamSlice(range[0], range[1] + 1)),
459
+ range: range = `bytes ${range[0]}-${range[1]}/${totalLength || '*'}`,
460
+ length: partLength,
461
+ });
462
+ _ranges.totalLength += partLength;
463
+ return _ranges;
464
+ }, { parts: [], totalLength: 0 });
465
+ if (ranges.error) {
466
+ response.attrs.status = 416;
467
+ response.headers.json({
468
+ 'Content-Range': `bytes */${totalLength || '*'}`,
469
+ 'Content-Length': 0,
470
+ });
471
+ } else {
472
+ // TODO: of ranges.parts is more than one, return multipart/byteranges
473
+ response = new Response(ranges.parts[0].body, {
474
+ status: 206,
475
+ statusText: response.statusText,
476
+ headers: response.headers,
477
+ });
478
+ response.headers.json({
479
+ 'Content-Range': ranges.parts[0].range,
480
+ 'Content-Length': ranges.totalLength,
481
+ });
482
+ }
483
+ }
484
+
485
+ return response;
486
+ }
487
+
488
+ // Generates log
489
+ generateLog(e, response) {
490
+ let log = [];
491
+ // ---------------
492
+ let style = this.cx.logger.style || { keyword: str => str, comment: str => str, url: str => str, val: str => str, err: str => str, };
493
+ let errorCode = [ 404, 500 ].includes(response.status) ? response.status : 0;
494
+ let xRedirectCode = response.headers.get('X-Redirect-Code');
495
+ let redirectCode = xRedirectCode || ((response.status + '').startsWith('3') ? response.status : 0);
496
+ let statusCode = xRedirectCode || response.status;
497
+ // ---------------
498
+ log.push(`[${style.comment((new Date).toUTCString())}]`);
499
+ log.push(style.keyword(e.request.method));
500
+ log.push(style.url(e.request.url));
501
+ if (response.attrs.hint) log.push(`(${style.comment(response.attrs.hint)})`);
502
+ if (response.headers.contentType) log.push(`(${style.comment(response.headers.contentType)})`);
503
+ if (errorCode) log.push(style.err(`${errorCode} ${response.statusText}`));
504
+ else log.push(style.val(`${statusCode} ${response.statusText}`));
505
+ if (redirectCode) log.push(`- ${style.url(response.headers.redirect)}`);
506
+
507
+ return log.join(' ');
508
+ }
509
+
510
+ // Applies auto headers
511
+ _autoHeaders(headers, autoHeaders) {
512
+ autoHeaders.forEach(header => {
513
+ var headerName = header.name.toLowerCase(),
514
+ headerValue = header.value,
515
+ isAppend = headerName.startsWith('+') ? (headerName = headerName.substr(1), true) : false,
516
+ isPrepend = headerName.endsWith('+') ? (headerName = headerName.substr(0, headerName.length - 1), true) : false;
517
+ if (isAppend || isPrepend) {
518
+ headerValue = [ headers.get(headerName) || '' , headerValue].filter(v => v);
519
+ headerValue = isPrepend ? headerValue.reverse().join(',') : headerValue.join(',');
520
+ }
521
+ headers.set(headerName, headerValue);
522
+ });
523
+ }
524
+
525
+ }
526
+
527
+ const _streamRead = stream => new Promise(res => {
528
+ let data = '';
529
+ stream.on('data', chunk => data += chunk);
530
+ stream.on('end', () => res(data));
531
+ });