@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.
- package/package.json +5 -12
- package/src/Cli.js +131 -0
- package/src/Configurator.js +97 -0
- package/src/Context.js +76 -0
- package/src/config-pi/deployment/Env.js +69 -0
- package/src/config-pi/deployment/Layout.js +65 -0
- package/src/config-pi/deployment/Origins.js +133 -0
- package/src/config-pi/deployment/Virtualization.js +65 -0
- package/src/config-pi/deployment/index.js +18 -0
- package/src/config-pi/index.js +16 -0
- package/src/config-pi/runtime/Client.js +59 -0
- package/src/config-pi/runtime/Server.js +174 -0
- package/src/config-pi/runtime/client/Worker.js +117 -0
- package/src/config-pi/runtime/client/index.js +12 -0
- package/src/config-pi/runtime/index.js +18 -0
- package/src/config-pi/runtime/server/Headers.js +90 -0
- package/src/config-pi/runtime/server/Redirects.js +108 -0
- package/src/config-pi/runtime/server/index.js +14 -0
- package/src/config-pi/static/Manifest.js +321 -0
- package/src/config-pi/static/Ssg.js +72 -0
- package/src/config-pi/static/index.js +14 -0
- package/src/deployment-pi/index.js +10 -0
- package/src/{services → deployment-pi}/origins/index.js +88 -58
- package/src/index.js +14 -147
- package/src/{runtime → runtime-pi}/Router.js +19 -19
- package/src/runtime-pi/client/Context.js +7 -0
- package/src/{runtime → runtime-pi}/client/Router.js +2 -2
- package/src/{runtime/client/Navigator.js → runtime-pi/client/Runtime.js} +143 -102
- package/src/runtime-pi/client/RuntimeClient.js +114 -0
- package/src/{runtime → runtime-pi}/client/Storage.js +8 -7
- package/src/{runtime → runtime-pi}/client/Url.js +2 -6
- package/src/{runtime/client/WorkerClient.js → runtime-pi/client/WorkerComm.js} +2 -2
- package/src/runtime-pi/client/generate.js +242 -0
- package/src/runtime-pi/client/generate.oohtml.js +7 -0
- package/src/runtime-pi/client/index.js +18 -0
- package/src/runtime-pi/client/whatwag.js +27 -0
- package/src/runtime-pi/client/worker/Context.js +7 -0
- package/src/runtime-pi/client/worker/Worker.js +243 -0
- package/src/runtime-pi/client/worker/WorkerClient.js +46 -0
- package/src/runtime-pi/client/worker/index.js +18 -0
- package/src/runtime-pi/index.js +14 -0
- package/src/runtime-pi/server/Context.js +16 -0
- package/src/{runtime → runtime-pi}/server/Router.js +6 -6
- package/src/runtime-pi/server/Runtime.js +531 -0
- package/src/runtime-pi/server/RuntimeClient.js +102 -0
- package/src/runtime-pi/server/index.js +41 -0
- package/src/runtime-pi/server/whatwag.js +35 -0
- package/src/{runtime → runtime-pi}/util.js +0 -0
- package/src/{runtime/_FormData.js → runtime-pi/xFormData.js} +2 -2
- package/src/{runtime/_Headers.js → runtime-pi/xHeaders.js} +4 -4
- package/src/runtime-pi/xHttpEvent.js +93 -0
- package/src/runtime-pi/xHttpMessage.js +179 -0
- package/src/runtime-pi/xRequest.js +67 -0
- package/src/runtime-pi/xRequestHeaders.js +95 -0
- package/src/runtime-pi/xResponse.js +62 -0
- package/src/{runtime/_ResponseHeaders.js → runtime-pi/xResponseHeaders.js} +38 -18
- package/src/{runtime/_URL.js → runtime-pi/xURL.js} +4 -4
- package/src/runtime-pi/xfetch.js +7 -0
- package/src/{services → services-pi}/certbot/http-auth-hook.js +0 -0
- package/src/{services → services-pi}/certbot/http-cleanup-hook.js +0 -0
- package/src/{services → services-pi}/certbot/index.js +21 -15
- package/src/services-pi/index.js +9 -0
- package/src/static-pi/index.js +11 -0
- package/src/webflo.js +33 -0
- package/test/index.test.js +26 -0
- package/src/build/client/index.js +0 -261
- package/src/build/index.js +0 -5
- package/src/config/client.js +0 -191
- package/src/config/headers.js +0 -121
- package/src/config/index.js +0 -14
- package/src/config/layout.js +0 -83
- package/src/config/manifest.js +0 -341
- package/src/config/origins.js +0 -165
- package/src/config/prerendering.js +0 -100
- package/src/config/redirects.js +0 -137
- package/src/config/server.js +0 -201
- package/src/config/variables.js +0 -102
- package/src/config/vhosts.js +0 -93
- package/src/runtime/_MessageStream.js +0 -195
- package/src/runtime/_NavigationEvent.js +0 -91
- package/src/runtime/_Request.js +0 -61
- package/src/runtime/_RequestHeaders.js +0 -72
- package/src/runtime/_Response.js +0 -56
- package/src/runtime/client/Cache.js +0 -38
- package/src/runtime/client/Http.js +0 -225
- package/src/runtime/client/NavigationEvent.js +0 -21
- package/src/runtime/client/Runtime.js +0 -126
- package/src/runtime/client/StdRequest.js +0 -74
- package/src/runtime/client/Worker.js +0 -312
- package/src/runtime/client/WorkerComm.js +0 -183
- package/src/runtime/client/effects/sounds.js +0 -64
- package/src/runtime/index.js +0 -5
- package/src/runtime/server/NavigationEvent.js +0 -39
- package/src/runtime/server/Runtime.js +0 -593
- package/src/runtime/server/index.js +0 -183
- package/src/runtime/server/index.mjs +0 -10
- package/src/services/index.js +0 -6
|
@@ -6,30 +6,31 @@
|
|
|
6
6
|
import { _isString, _isUndefined } from '@webqit/util/js/index.js';
|
|
7
7
|
import { Observer } from './Runtime.js';
|
|
8
8
|
|
|
9
|
-
export default function(persistent = false) {
|
|
9
|
+
export default function(namespace = null, persistent = false) {
|
|
10
10
|
|
|
11
11
|
const storeType = persistent ? 'localStorage' : 'sessionStorage';
|
|
12
12
|
if (!window[storeType]) {
|
|
13
|
-
throw new Error(`The specified Web Storage API ${storeType} is invalid or not supported`)
|
|
13
|
+
throw new Error(`The specified Web Storage API ${storeType} is invalid or not supported`);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
const _storage = {};
|
|
17
17
|
Observer.intercept(_storage, (event, received, next) => {
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
const key = namespace ? `${namespace}.${event.name}` : event.name;
|
|
19
|
+
if (event.type === 'get' && _isString(key)) {
|
|
20
|
+
const value = window[storeType].getItem(key);
|
|
20
21
|
return !_isUndefined(value) ? JSON.parse(value) : value;
|
|
21
22
|
}
|
|
22
23
|
if (event.type === 'set') {
|
|
23
|
-
window[storeType].setItem(
|
|
24
|
+
window[storeType].setItem(key, !_isUndefined(event.value) ? JSON.stringify(event.value) : event.value);
|
|
24
25
|
return true;
|
|
25
26
|
}
|
|
26
27
|
if (event.type === 'deleteProperty') {
|
|
27
|
-
window[storeType].removeItem(
|
|
28
|
+
window[storeType].removeItem(key);
|
|
28
29
|
return true;
|
|
29
30
|
}
|
|
30
31
|
if (event.type === 'has') {
|
|
31
32
|
for(var i = 0; i < window[storeType].length; i ++){
|
|
32
|
-
if (window[storeType].key(i) ===
|
|
33
|
+
if (window[storeType].key(i) === key) {
|
|
33
34
|
return true;
|
|
34
35
|
}
|
|
35
36
|
};
|
|
@@ -2,12 +2,8 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* @imports
|
|
4
4
|
*/
|
|
5
|
-
import
|
|
6
|
-
import _isObject from '@webqit/util/js/
|
|
7
|
-
import _isTypeObject from '@webqit/util/js/isTypeObject.js';
|
|
8
|
-
import _isString from '@webqit/util/js/isString.js';
|
|
9
|
-
import _isEmpty from '@webqit/util/js/isEmpty.js';
|
|
10
|
-
import _with from '@webqit/util/obj/with.js';
|
|
5
|
+
import { _with } from '@webqit/util/obj/index.js';
|
|
6
|
+
import { _isArray, _isObject, _isTypeObject, _isString, _isEmpty } from '@webqit/util/js/index.js';
|
|
11
7
|
import { wwwFormUnserialize, wwwFormSerialize } from '../util.js';
|
|
12
8
|
import { Observer } from './Runtime.js';
|
|
13
9
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { _isFunction } from '@webqit/util/js/index.js';
|
|
7
7
|
import { Observer } from './Runtime.js';
|
|
8
8
|
|
|
9
|
-
export default class
|
|
9
|
+
export default class WorkerComm {
|
|
10
10
|
|
|
11
11
|
constructor(file, params = {}) {
|
|
12
12
|
this.ready = navigator.serviceWorker.ready;
|
|
@@ -73,7 +73,7 @@ export default class WorkerClient {
|
|
|
73
73
|
}
|
|
74
74
|
return this.post;
|
|
75
75
|
},
|
|
76
|
-
receive:
|
|
76
|
+
receive: callback => {
|
|
77
77
|
navigator.serviceWorker.addEventListener('message', callback);
|
|
78
78
|
return this.post;
|
|
79
79
|
},
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* imports
|
|
4
|
+
*/
|
|
5
|
+
import Fs from 'fs';
|
|
6
|
+
import Url from 'url';
|
|
7
|
+
import Path from 'path';
|
|
8
|
+
import Webpack from 'webpack';
|
|
9
|
+
import { _beforeLast } from '@webqit/util/str/index.js';
|
|
10
|
+
import { _isObject, _isArray } from '@webqit/util/js/index.js';
|
|
11
|
+
import * as DotJs from '@webqit/backpack/src/dotfiles/DotJs.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @generate
|
|
15
|
+
*/
|
|
16
|
+
export async function generate() {
|
|
17
|
+
const cx = this || {};
|
|
18
|
+
// -----------
|
|
19
|
+
if (!cx.config.runtime?.Client) {
|
|
20
|
+
throw new Error(`The Client configurator "config.runtime.Client" is required in context.`);
|
|
21
|
+
}
|
|
22
|
+
const clientConfig = await (new cx.config.runtime.Client(cx)).read();
|
|
23
|
+
if (clientConfig.support_service_worker && !cx.config.runtime.client?.Worker) {
|
|
24
|
+
throw new Error(`The Service Worker configurator "config.runtime.client.Worker" is required in context.`);
|
|
25
|
+
}
|
|
26
|
+
const workerConfig = await (new cx.config.runtime.client.Worker(cx)).read();
|
|
27
|
+
// -----------
|
|
28
|
+
if (!cx.config.deployment?.Layout) {
|
|
29
|
+
throw new Error(`The Layout configurator "config.deployment.Layout" is required in context.`);
|
|
30
|
+
}
|
|
31
|
+
const layoutConfig = await (new cx.config.deployment.Layout(cx)).read();
|
|
32
|
+
const bundleOutput = { path: Path.resolve(cx.CWD || '', layoutConfig.PUBLIC_DIR), };
|
|
33
|
+
const dirSelf = Path.dirname(Url.fileURLToPath(import.meta.url)).replace(/\\/g, '/');
|
|
34
|
+
// -----------
|
|
35
|
+
// Generate client build
|
|
36
|
+
let genClient = getGen.call(cx, dirSelf, layoutConfig.CLIENT_DIR, clientConfig, `The Client Build.`);
|
|
37
|
+
if (clientConfig.support_oohtml) {
|
|
38
|
+
genClient.imports = { [`${dirSelf}/generate.oohtml.js`]: null, ...genClient.imports };
|
|
39
|
+
}
|
|
40
|
+
await bundle.call(cx, genClient, { ...bundleOutput, filename: 'bundle.js', }, true/* asModule */);
|
|
41
|
+
cx.logger && cx.logger.log('');
|
|
42
|
+
// -----------
|
|
43
|
+
// Generate worker build
|
|
44
|
+
if (clientConfig.support_service_worker) {
|
|
45
|
+
let genWorker = getGen.call(cx, `${dirSelf}/worker`, layoutConfig.WORKER_DIR, workerConfig, `The Worker Build.`);
|
|
46
|
+
await bundle.call(cx, genWorker, { ...bundleOutput, filename: 'worker.js', });
|
|
47
|
+
cx.logger && cx.logger.log('');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Compile routes.
|
|
53
|
+
*
|
|
54
|
+
* @param string modulesDir
|
|
55
|
+
* @param string routesDir
|
|
56
|
+
* @param object paramsObj
|
|
57
|
+
* @param string desc
|
|
58
|
+
*
|
|
59
|
+
* @return Object
|
|
60
|
+
*/
|
|
61
|
+
function getGen(modulesDir, routesDir, paramsObj, desc) {
|
|
62
|
+
const cx = this || {};
|
|
63
|
+
if (cx.logger) {
|
|
64
|
+
cx.logger.log(cx.logger.style.comment(`-----------------`));
|
|
65
|
+
cx.logger.log(desc);
|
|
66
|
+
cx.logger.log(cx.logger.style.comment(`-----------------`));
|
|
67
|
+
cx.logger.log('');
|
|
68
|
+
}
|
|
69
|
+
// ------------------
|
|
70
|
+
const gen = { imports: {}, code: [], };
|
|
71
|
+
// ------------------
|
|
72
|
+
// >> Modules import
|
|
73
|
+
gen.imports[`${modulesDir}/index.js`] = `{ start }`;
|
|
74
|
+
gen.code.push(``);
|
|
75
|
+
// ------------------
|
|
76
|
+
// >> Routes mapping
|
|
77
|
+
gen.code.push(`// >> Routes`);
|
|
78
|
+
declareRoutesObj.call(cx, gen, routesDir, 'layout', '');
|
|
79
|
+
gen.code.push(``);
|
|
80
|
+
// ------------------
|
|
81
|
+
// >> Params
|
|
82
|
+
gen.code.push(`// >> Params`);
|
|
83
|
+
declareParamsObj.call(cx, gen, paramsObj, 'params');
|
|
84
|
+
gen.code.push(``);
|
|
85
|
+
// ------------------
|
|
86
|
+
// >> Startup
|
|
87
|
+
gen.code.push(`// >> Startup`);
|
|
88
|
+
gen.code.push(`start.call({ layout, params })`);
|
|
89
|
+
return gen;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Compile routes.
|
|
94
|
+
*
|
|
95
|
+
* @param object gen
|
|
96
|
+
* @param string routesDir
|
|
97
|
+
* @param string varName
|
|
98
|
+
*
|
|
99
|
+
* @return void
|
|
100
|
+
*/
|
|
101
|
+
function declareRoutesObj(gen, routesDir, varName) {
|
|
102
|
+
const cx = this || {};
|
|
103
|
+
cx.logger && cx.logger.log(`> Declaring routes...`);
|
|
104
|
+
// ----------------
|
|
105
|
+
// Directory walker
|
|
106
|
+
const walk = (dir, callback) => {
|
|
107
|
+
Fs.readdirSync(dir).forEach(f => {
|
|
108
|
+
let resource = Path.join(dir, f);
|
|
109
|
+
if (Fs.statSync(resource).isDirectory()) {
|
|
110
|
+
walk(resource, callback);
|
|
111
|
+
} else {
|
|
112
|
+
let ext = Path.extname(resource) || '';
|
|
113
|
+
callback(resource, ext);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
// ----------------
|
|
118
|
+
// >> Routes mapping
|
|
119
|
+
gen.code.push(`const ${varName} = {};`);
|
|
120
|
+
let indexCount = 0;
|
|
121
|
+
if (routesDir && Fs.existsSync(routesDir)) {
|
|
122
|
+
let clientDirname = routesDir.replace(/\\/g, '/').split('/').pop();
|
|
123
|
+
walk(routesDir, (file, ext) => {
|
|
124
|
+
//relativePath = relativePath.replace(/\\/g, '/');
|
|
125
|
+
if (file.replace(/\\/g, '/').endsWith('/index.js')) {
|
|
126
|
+
let relativePath = Path.relative(routesDir, file).replace(/\\/g, '/');
|
|
127
|
+
// Import code
|
|
128
|
+
let routeName = 'index' + (++ indexCount);
|
|
129
|
+
// IMPORTANT: we;re taking a step back here so that the parent-child relationship for
|
|
130
|
+
// the directories be involved
|
|
131
|
+
gen.imports[`../${clientDirname}/${relativePath}`] = '* as ' + routeName;
|
|
132
|
+
// Definition code
|
|
133
|
+
let routePath = _beforeLast('/' + relativePath, '/index.js');
|
|
134
|
+
gen.code.push(`${varName}['${routePath || '/'}'] = ${routeName};`);
|
|
135
|
+
// Show
|
|
136
|
+
cx.logger && cx.logger.log(`> ./${relativePath}`);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
if (!indexCount) {
|
|
141
|
+
cx.logger && cx.logger.log(`> (none)`);
|
|
142
|
+
}
|
|
143
|
+
cx.logger && cx.logger.log(``);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Compile params.
|
|
148
|
+
*
|
|
149
|
+
* @param object gen
|
|
150
|
+
* @param object paramsObj
|
|
151
|
+
* @param string varName
|
|
152
|
+
*
|
|
153
|
+
* @return void
|
|
154
|
+
*/
|
|
155
|
+
function declareParamsObj(gen, paramsObj, varName = null, indentation = 0) {
|
|
156
|
+
const cx = this || {};
|
|
157
|
+
// ----------------
|
|
158
|
+
// Params compilation
|
|
159
|
+
if (varName) gen.code.push(`const ${varName} = {`);
|
|
160
|
+
_isArray(paramsObj)
|
|
161
|
+
Object.keys(paramsObj).forEach(name => {
|
|
162
|
+
let _name = ` ${' '.repeat(indentation)}${(_isArray(paramsObj) ? '' : (name.includes(' ') ? `'${name}'` : name) + ': ')}`;
|
|
163
|
+
if ([ 'boolean', 'number' ].includes(typeof paramsObj[name])) {
|
|
164
|
+
gen.code.push(`${_name}${paramsObj[name]},`);
|
|
165
|
+
} else if (_isArray(paramsObj[name])) {
|
|
166
|
+
gen.code.push(`${_name}[`);
|
|
167
|
+
declareParamsObj.call(cx, gen, paramsObj[name], null, indentation + 1);
|
|
168
|
+
gen.code.push(` ${' '.repeat(indentation)}],`);
|
|
169
|
+
} else if (_isObject(paramsObj[name])) {
|
|
170
|
+
gen.code.push(`${_name}{`);
|
|
171
|
+
declareParamsObj.call(cx, gen, paramsObj[name], null, indentation + 1);
|
|
172
|
+
gen.code.push(` ${' '.repeat(indentation)}},`);
|
|
173
|
+
} else {
|
|
174
|
+
gen.code.push(`${_name}'${paramsObj[name]}',`);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
if (varName) gen.code.push(`};`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Bundle generated file
|
|
182
|
+
*
|
|
183
|
+
* @param object gen
|
|
184
|
+
* @param object output
|
|
185
|
+
* @param boolean asModule
|
|
186
|
+
*
|
|
187
|
+
* @return Promise
|
|
188
|
+
*/
|
|
189
|
+
function bundle(gen, output, asModule = false) {
|
|
190
|
+
const cx = this || {};
|
|
191
|
+
const moduleFile = Path.join(output.path, `${_beforeLast(output.filename, '.')}.esm.js`);
|
|
192
|
+
// ------------------
|
|
193
|
+
// >> Show waiting...
|
|
194
|
+
if (cx.logger) {
|
|
195
|
+
let waiting = cx.logger.waiting(cx.logger.f`Writing the ES module file: ${moduleFile}`);
|
|
196
|
+
waiting.start();
|
|
197
|
+
DotJs.write(gen, moduleFile, 'ES Module file');
|
|
198
|
+
waiting.stop();
|
|
199
|
+
cx.logger.info(cx.logger.f`The module file: ${moduleFile}`);
|
|
200
|
+
} else {
|
|
201
|
+
DotJs.write(gen, moduleFile, 'ES Module file');
|
|
202
|
+
}
|
|
203
|
+
// ----------------
|
|
204
|
+
// >> Webpack config
|
|
205
|
+
const bundlingConfig = { entry: moduleFile, output };
|
|
206
|
+
if (asModule) {
|
|
207
|
+
bundlingConfig.experiments = { outputModule: true, };
|
|
208
|
+
bundlingConfig.output.environment = bundlingConfig.output.environment || {};
|
|
209
|
+
if (!('module' in bundlingConfig.output)) {
|
|
210
|
+
bundlingConfig.output.module = true;
|
|
211
|
+
bundlingConfig.output.environment.module = true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// ----------------
|
|
215
|
+
// The bundling process
|
|
216
|
+
return new Promise(resolve => {
|
|
217
|
+
let waiting;
|
|
218
|
+
if (cx.logger) {
|
|
219
|
+
waiting = cx.logger.waiting(`Bundling...`);
|
|
220
|
+
cx.logger.log('');
|
|
221
|
+
cx.logger.log('> Bundling...');
|
|
222
|
+
cx.logger.info(cx.logger.f`FROM: ${bundlingConfig.entry}`);
|
|
223
|
+
cx.logger.info(cx.logger.f`TO: ${bundlingConfig.output.path + '/' + bundlingConfig.output.filename}`);
|
|
224
|
+
cx.logger.log('');
|
|
225
|
+
waiting.start();
|
|
226
|
+
}
|
|
227
|
+
// Run
|
|
228
|
+
let compiler = Webpack(bundlingConfig);
|
|
229
|
+
compiler.run((err, stats) => {
|
|
230
|
+
waiting.stop();
|
|
231
|
+
if (err) {
|
|
232
|
+
cx.logger.title(`Errors!`);
|
|
233
|
+
cx.logger.error(err);
|
|
234
|
+
}
|
|
235
|
+
let log = stats.toString({ colors: true, });
|
|
236
|
+
cx.logger && cx.logger.log(log);
|
|
237
|
+
// Remove moduleFile build
|
|
238
|
+
//Fs.unlinkSync(bundlingConfig.entry);
|
|
239
|
+
resolve(log);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* @imports
|
|
4
|
+
*/
|
|
5
|
+
import Context from './Context.js';
|
|
6
|
+
import RuntimeClient from './RuntimeClient.js';
|
|
7
|
+
import Runtime from './Runtime.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @start
|
|
11
|
+
*/
|
|
12
|
+
export async function start(clientCallback = null) {
|
|
13
|
+
const cx = this || {};
|
|
14
|
+
const defaultClientCallback = _cx => new RuntimeClient(_cx);
|
|
15
|
+
return new Runtime(Context.create(cx), ( ...args ) => {
|
|
16
|
+
return clientCallback ? clientCallback( ...args.concat( defaultClientCallback ) ) : defaultClientCallback( ...args );
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
|
|
2
|
+
const {
|
|
3
|
+
URL,
|
|
4
|
+
fetch,
|
|
5
|
+
Headers,
|
|
6
|
+
Request,
|
|
7
|
+
Response,
|
|
8
|
+
FormData,
|
|
9
|
+
ReadableStream,
|
|
10
|
+
File,
|
|
11
|
+
Blob
|
|
12
|
+
} = globalThis;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @exports
|
|
16
|
+
*/
|
|
17
|
+
export {
|
|
18
|
+
URL,
|
|
19
|
+
fetch,
|
|
20
|
+
Headers,
|
|
21
|
+
Request,
|
|
22
|
+
Response,
|
|
23
|
+
FormData,
|
|
24
|
+
ReadableStream,
|
|
25
|
+
File,
|
|
26
|
+
Blob,
|
|
27
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* @imports
|
|
4
|
+
*/
|
|
5
|
+
import _isGlobe from 'is-glob';
|
|
6
|
+
import Minimatch from 'minimatch';
|
|
7
|
+
import { _any } from '@webqit/util/arr/index.js';
|
|
8
|
+
import { _after, _afterLast } from '@webqit/util/str/index.js';
|
|
9
|
+
import { HttpEvent, Request, Response } from '../Runtime.js';
|
|
10
|
+
import { Observer } from '../Runtime.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* ---------------------------
|
|
14
|
+
* The Worker Initializer
|
|
15
|
+
* ---------------------------
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export default class Worker {
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Runtime
|
|
22
|
+
*
|
|
23
|
+
* @param Object cx
|
|
24
|
+
* @param Function clientCallback
|
|
25
|
+
*
|
|
26
|
+
* @return void
|
|
27
|
+
*/
|
|
28
|
+
constructor(cx, clientCallback) {
|
|
29
|
+
|
|
30
|
+
// ---------------
|
|
31
|
+
this.cx = cx;
|
|
32
|
+
this.clients = new Map;
|
|
33
|
+
this.mockSessionStore = {};
|
|
34
|
+
// ---------------
|
|
35
|
+
this.cx.runtime = this;
|
|
36
|
+
let client = clientCallback(this.cx, '*');
|
|
37
|
+
if (!client || !client.handle) throw new Error(`Application instance must define a ".handle()" method.`);
|
|
38
|
+
this.clients.set('*', client);
|
|
39
|
+
|
|
40
|
+
// -------------
|
|
41
|
+
// ONINSTALL
|
|
42
|
+
self.addEventListener('install', evt => {
|
|
43
|
+
if (this.cx.params.skip_waiting) { self.skipWaiting(); }
|
|
44
|
+
// Manage CACHE
|
|
45
|
+
if (this.cx.params.cache_name && (this.cx.params.cache_only_urls || []).length) {
|
|
46
|
+
// Add files to cache
|
|
47
|
+
evt.waitUntil( self.caches.open(this.cx.params.cache_name).then(cache => {
|
|
48
|
+
if (this.cx.logger) { this.cx.logger.log('[ServiceWorker] Pre-caching resources.'); }
|
|
49
|
+
const cache_only_urls = (this.cx.params.cache_only_urls || []).map(c => c.trim()).filter(c => c);
|
|
50
|
+
return cache.addAll(cache_only_urls.filter(url => !_isGlobe(url) && !_afterLast(url, '.').includes('/')));
|
|
51
|
+
}) );
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// -------------
|
|
56
|
+
// ONACTIVATE
|
|
57
|
+
self.addEventListener('activate', evt => {
|
|
58
|
+
evt.waitUntil( new Promise(async resolve => {
|
|
59
|
+
if (this.cx.params.skip_waiting) { await self.clients.claim(); }
|
|
60
|
+
// Manage CACHE
|
|
61
|
+
if (this.cx.params.cache_name) {
|
|
62
|
+
// Clear outdated CACHES
|
|
63
|
+
await self.caches.keys().then(keyList => {
|
|
64
|
+
return Promise.all(keyList.map(key => {
|
|
65
|
+
if (key !== this.cx.params.cache_name && key !== this.cx.params.cache_name + '_json') {
|
|
66
|
+
if (this.cx.logger) { this.cx.logger.log('[ServiceWorker] Removing old cache:', key); }
|
|
67
|
+
return self.caches.delete(key);
|
|
68
|
+
}
|
|
69
|
+
}));
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
resolve();
|
|
73
|
+
}) );
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// -------------
|
|
77
|
+
// ONFETCH
|
|
78
|
+
self.addEventListener('fetch', async evt => {
|
|
79
|
+
// URL schemes that might arrive here but not supported; e.g.: chrome-extension://
|
|
80
|
+
if (!evt.request.url.startsWith('http')) return;
|
|
81
|
+
const requestInit = [
|
|
82
|
+
'method', 'headers', 'body', 'mode', 'credentials', 'cache', 'redirect', 'referrer', 'integrity',
|
|
83
|
+
].reduce((init, prop) => ({ [prop]: evt.request[prop], ...init }), {});
|
|
84
|
+
evt.respondWith(this.go(evt.request.url, requestInit, { event: evt }));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// ---------------
|
|
88
|
+
Observer.set(this, 'location', {});
|
|
89
|
+
Observer.set(this, 'network', {});
|
|
90
|
+
// ---------------
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Performs a request.
|
|
95
|
+
*
|
|
96
|
+
* @param object|string url
|
|
97
|
+
* @param object init
|
|
98
|
+
* @param object detail
|
|
99
|
+
*
|
|
100
|
+
* @return Response
|
|
101
|
+
*/
|
|
102
|
+
async go(url, init = {}, detail = {}) {
|
|
103
|
+
// ------------
|
|
104
|
+
url = typeof url === 'string' ? new URL(url) : url;
|
|
105
|
+
init = { referrer: this.location.href, ...init };
|
|
106
|
+
// ------------
|
|
107
|
+
// The request object
|
|
108
|
+
let request = this.generateRequest(url.href, init);
|
|
109
|
+
// The navigation event
|
|
110
|
+
let httpEvent = new HttpEvent(request, detail, (id = null, persistent = false) => this.getSession(httpEvent, id, persistent));
|
|
111
|
+
httpEvent.port.listen(message => {
|
|
112
|
+
if (message.$type === 'handler:hints' && message.session) {
|
|
113
|
+
// TODO: Sync sesseion data from client
|
|
114
|
+
return Promise.resolve();
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
// Response
|
|
118
|
+
let response;
|
|
119
|
+
if (httpEvent.request.url.startsWith(self.origin)/* && httpEvent.request.mode === 'navigate'*/) {
|
|
120
|
+
response = await this.clients.get('*').handle(httpEvent, (...args) => this.remoteFetch(...args));
|
|
121
|
+
} else {
|
|
122
|
+
response = await this.remoteFetch(httpEvent.request);
|
|
123
|
+
}
|
|
124
|
+
let finalResponse = this.handleResponse(httpEvent, response);
|
|
125
|
+
// Return value
|
|
126
|
+
return finalResponse;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Generates request object
|
|
130
|
+
generateRequest(href, init) {
|
|
131
|
+
// Now, the following is key:
|
|
132
|
+
// The browser likes to use "force-cache" for "navigate" requests
|
|
133
|
+
// when, for example, the back button was used.
|
|
134
|
+
// Thus the origin server would still not be contacted by the self.fetch() below, leading to inconsistencies in responses.
|
|
135
|
+
// So, we detect this scenerio and avoid it.
|
|
136
|
+
if (init.mode === 'navigate' && init.cache === 'force-cache') {
|
|
137
|
+
init = { ...init, cache: 'default' };
|
|
138
|
+
}
|
|
139
|
+
let request = new Request(href, init);
|
|
140
|
+
return request;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Generates session object
|
|
144
|
+
getSession(e, id = null, persistent = false) {
|
|
145
|
+
return {
|
|
146
|
+
get: () => this.mockSessionStore,
|
|
147
|
+
set: value => { this.mockSessionStore = value },
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Initiates remote fetch and sets the status
|
|
152
|
+
remoteFetch(request) {
|
|
153
|
+
const execFetch = () => {
|
|
154
|
+
if (_any((this.cx.params.cache_only_urls || []).map(c => c.trim()).filter(c => c), pattern => Minimatch.Minimatch(request.url, pattern))) {
|
|
155
|
+
Observer.set(this.network, 'strategy', 'cache-only');
|
|
156
|
+
return this.cacheFetch(request, { networkFallback: false, cacheRefresh: false });
|
|
157
|
+
}
|
|
158
|
+
// network_only_urls
|
|
159
|
+
if (_any((this.cx.params.network_only_urls || []).map(c => c.trim()).filter(c => c), pattern => Minimatch.Minimatch(request.url, pattern))) {
|
|
160
|
+
Observer.set(this.network, 'strategy', 'network-only');
|
|
161
|
+
return this.networkFetch(request, { cacheFallback: false, cacheRefresh: false });
|
|
162
|
+
}
|
|
163
|
+
// cache_first_urls
|
|
164
|
+
if (_any((this.cx.params.cache_first_urls || []).map(c => c.trim()).filter(c => c), pattern => Minimatch.Minimatch(request.url, pattern))) {
|
|
165
|
+
Observer.set(this.network, 'strategy', 'cache-first');
|
|
166
|
+
return this.cacheFetch(request, { networkFallback: true, cacheRefresh: true });
|
|
167
|
+
}
|
|
168
|
+
Observer.set(this.network, 'strategy', 'network-first');
|
|
169
|
+
return this.networkFetch(request, { cacheFallback: true, cacheRefresh: true });
|
|
170
|
+
};
|
|
171
|
+
let response = execFetch(request);
|
|
172
|
+
// This catch() is NOT intended to handle failure of the fetch
|
|
173
|
+
response.catch(e => Observer.set(this.network, 'error', e.message));
|
|
174
|
+
// Return xResponse
|
|
175
|
+
return response.then(_response => new Response(_response));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Caching strategy: cache_first
|
|
179
|
+
cacheFetch(request, params = {}) {
|
|
180
|
+
return this.getRequestCache(request).then(cache => cache.match(request).then(response => {
|
|
181
|
+
// Nothing cache, use network
|
|
182
|
+
if (!response && params.networkFallback) return this.networkFetch(request, { ...params, cacheFallback: false });
|
|
183
|
+
// Note: fetch, but for refreshing purposes only... not the returned response
|
|
184
|
+
if (response && params.cacheRefresh) this.networkFetch(request, { ...params, justRefreshing: true });
|
|
185
|
+
Observer.set(this.network, 'cache', true);
|
|
186
|
+
return response;
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Caching strategy: network_first
|
|
191
|
+
networkFetch(request, params = {}) {
|
|
192
|
+
if (params.forceNetwork) {
|
|
193
|
+
let url = new URL(request.url);
|
|
194
|
+
url.searchParams.set('$force-cache', '1');
|
|
195
|
+
request.attr.url = url.toString();
|
|
196
|
+
}
|
|
197
|
+
if (!params.cacheFallback) {
|
|
198
|
+
Observer.set(this.network, 'remote', true);
|
|
199
|
+
return self.fetch(request);
|
|
200
|
+
}
|
|
201
|
+
return self.fetch(request).then(response => {
|
|
202
|
+
if (params.cacheRefresh) this.refreshCache(request, response);
|
|
203
|
+
Observer.set(this.network, 'remote', true);
|
|
204
|
+
return response;
|
|
205
|
+
}).catch(() => this.getRequestCache(request).then(cache => {
|
|
206
|
+
Observer.set(this.network, 'cache', true);
|
|
207
|
+
return cache.match(request);
|
|
208
|
+
}));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Caches response
|
|
212
|
+
refreshCache(request, response) {
|
|
213
|
+
// Check if we received a valid response
|
|
214
|
+
if (request.method !== 'GET' || !response || response.status !== 200 || (response.type !== 'basic' && response.type !== 'cors')) {
|
|
215
|
+
return response;
|
|
216
|
+
}
|
|
217
|
+
// IMPORTANT: Clone the response. A response is a stream
|
|
218
|
+
// and because we want the browser to consume the response
|
|
219
|
+
// as well as the cache consuming the response, we need
|
|
220
|
+
// to clone it so we have two streams.
|
|
221
|
+
var responseToCache = response.clone();
|
|
222
|
+
this.getRequestCache(request).then(cache => {
|
|
223
|
+
Observer.set(this.network, 'cacheRefresh', true);
|
|
224
|
+
cache.put(request, responseToCache);
|
|
225
|
+
});
|
|
226
|
+
return response;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Returns either the regular cache or a json-specific cache
|
|
230
|
+
getRequestCache(request) {
|
|
231
|
+
let cacheName = request.headers.get('Accept') === 'application/json'
|
|
232
|
+
? this.cx.params.cache_name + '_json'
|
|
233
|
+
: this.cx.params.cache_name;
|
|
234
|
+
return self.caches.open(cacheName);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Handles response object
|
|
238
|
+
handleResponse(e, response) {
|
|
239
|
+
if (!(response instanceof Response)) { response = new Response(response); }
|
|
240
|
+
return response;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* @imports
|
|
4
|
+
*/
|
|
5
|
+
import Router from '../Router.js';
|
|
6
|
+
|
|
7
|
+
export default class WorkerClient {
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* WorkerClient
|
|
11
|
+
*
|
|
12
|
+
* @param Context cx
|
|
13
|
+
*/
|
|
14
|
+
constructor(cx) {
|
|
15
|
+
this.cx = cx;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Handles HTTP events.
|
|
20
|
+
*
|
|
21
|
+
* @param HttpEvent httpEvent
|
|
22
|
+
* @param Function remoteFetch
|
|
23
|
+
*
|
|
24
|
+
* @return Response
|
|
25
|
+
*/
|
|
26
|
+
async handle(httpEvent, remoteFetch) {
|
|
27
|
+
// The app router
|
|
28
|
+
const router = new Router(this.cx, httpEvent.url.pathname);
|
|
29
|
+
const handle = async () => {
|
|
30
|
+
// --------
|
|
31
|
+
// ROUTE FOR DATA
|
|
32
|
+
// --------
|
|
33
|
+
let httpMethodName = httpEvent.request.method.toLowerCase();
|
|
34
|
+
let response = await router.route([httpMethodName === 'delete' ? 'del' : httpMethodName, 'default'], httpEvent, {}, async event => {
|
|
35
|
+
return remoteFetch(event.request);
|
|
36
|
+
}, remoteFetch);
|
|
37
|
+
if (!(response instanceof httpEvent.Response)) {
|
|
38
|
+
response = new httpEvent.Response(response);
|
|
39
|
+
}
|
|
40
|
+
return response;
|
|
41
|
+
};
|
|
42
|
+
return handle();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
}
|
|
46
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* @imports
|
|
4
|
+
*/
|
|
5
|
+
import Context from './Context.js';
|
|
6
|
+
import WorkerClient from './WorkerClient.js';
|
|
7
|
+
import Worker from './Worker.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @start
|
|
11
|
+
*/
|
|
12
|
+
export async function start(clientCallback = null) {
|
|
13
|
+
const cx = this || {};
|
|
14
|
+
const defaultClientCallback = _cx => new WorkerClient(_cx);
|
|
15
|
+
return new Worker(Context.create(cx), ( ...args ) => {
|
|
16
|
+
return clientCallback ? clientCallback( ...args.concat( defaultClientCallback ) ) : defaultClientCallback( ...args );
|
|
17
|
+
});
|
|
18
|
+
}
|