@webqit/webflo 0.20.4-next.2 → 0.20.4-next.4
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 +13 -34
- package/site/docs/concepts/realtime.md +45 -44
- package/site/docs/getting-started.md +40 -40
- package/src/{Context.js → CLIContext.js} +9 -8
- package/src/build-pi/esbuild-plugin-uselive-transform.js +42 -0
- package/src/{runtime-pi/webflo-client/webflo-codegen.js → build-pi/index.js} +148 -142
- package/src/index.js +3 -1
- package/src/init-pi/index.js +7 -4
- package/src/init-pi/templates/pwa/.gitignore +6 -0
- package/src/init-pi/templates/pwa/.webqit/webflo/client.json +15 -0
- package/src/init-pi/templates/pwa/.webqit/webflo/layout.json +7 -0
- package/src/init-pi/templates/pwa/package.json +2 -2
- package/src/init-pi/templates/pwa/public/manifest.json +2 -2
- package/src/init-pi/templates/web/.gitignore +6 -0
- package/src/init-pi/templates/web/.webqit/webflo/client.json +12 -0
- package/src/init-pi/templates/web/.webqit/webflo/layout.json +7 -0
- package/src/init-pi/templates/web/package.json +2 -2
- package/src/runtime-pi/AppBootstrap.js +38 -0
- package/src/runtime-pi/WebfloRuntime.js +68 -56
- package/src/runtime-pi/apis.js +9 -0
- package/src/runtime-pi/index.js +2 -4
- package/src/runtime-pi/webflo-client/DeviceCapabilities.js +1 -1
- package/src/runtime-pi/webflo-client/WebfloClient.js +33 -36
- package/src/runtime-pi/webflo-client/WebfloRootClient1.js +23 -17
- package/src/runtime-pi/webflo-client/WebfloRootClient2.js +1 -1
- package/src/runtime-pi/webflo-client/WebfloSubClient.js +14 -14
- package/src/runtime-pi/webflo-client/bootstrap.js +38 -0
- package/src/runtime-pi/webflo-client/index.js +2 -8
- package/src/runtime-pi/webflo-client/webflo-devmode.js +3 -3
- package/src/runtime-pi/webflo-fetch/LiveResponse.js +154 -116
- package/src/runtime-pi/webflo-fetch/index.js +436 -5
- package/src/runtime-pi/webflo-messaging/wq-message-port.js +1 -1
- package/src/runtime-pi/webflo-routing/HttpCookies.js +1 -1
- package/src/runtime-pi/webflo-routing/HttpEvent.js +12 -11
- package/src/runtime-pi/webflo-routing/HttpUser.js +7 -7
- package/src/runtime-pi/webflo-routing/WebfloRouter.js +12 -7
- package/src/runtime-pi/webflo-server/ServerSideCookies.js +3 -1
- package/src/runtime-pi/webflo-server/ServerSideSession.js +2 -1
- package/src/runtime-pi/webflo-server/WebfloServer.js +138 -200
- package/src/runtime-pi/webflo-server/bootstrap.js +59 -0
- package/src/runtime-pi/webflo-server/index.js +2 -6
- package/src/runtime-pi/webflo-server/webflo-devmode.js +24 -31
- package/src/runtime-pi/webflo-url/Url.js +1 -1
- package/src/runtime-pi/webflo-url/xURL.js +1 -1
- package/src/runtime-pi/webflo-worker/WebfloWorker.js +11 -15
- package/src/runtime-pi/webflo-worker/WorkerSideCookies.js +2 -1
- package/src/runtime-pi/webflo-worker/bootstrap.js +39 -0
- package/src/runtime-pi/webflo-worker/index.js +3 -7
- package/src/webflo-cli.js +1 -2
- package/src/runtime-pi/webflo-fetch/cookies.js +0 -10
- package/src/runtime-pi/webflo-fetch/fetch.js +0 -16
- package/src/runtime-pi/webflo-fetch/formdata.js +0 -54
- package/src/runtime-pi/webflo-fetch/headers.js +0 -151
- package/src/runtime-pi/webflo-fetch/message.js +0 -49
- package/src/runtime-pi/webflo-fetch/request.js +0 -62
- package/src/runtime-pi/webflo-fetch/response.js +0 -110
|
@@ -1,5 +1,436 @@
|
|
|
1
|
-
import '
|
|
2
|
-
import '
|
|
3
|
-
import '
|
|
4
|
-
import '
|
|
5
|
-
import './
|
|
1
|
+
import { _isObject, _isTypeObject, _isNumeric } from '@webqit/util/js/index.js';
|
|
2
|
+
import { _from as _arrFrom } from '@webqit/util/arr/index.js';
|
|
3
|
+
import { _before, _after } from '@webqit/util/str/index.js';
|
|
4
|
+
import { DeepURLSearchParams } from '../webflo-url/util.js';
|
|
5
|
+
import { dataType } from './util.js';
|
|
6
|
+
import { _wq } from '../../util.js';
|
|
7
|
+
import { Observer } from '@webqit/use-live';
|
|
8
|
+
import { LiveResponse } from './LiveResponse.js';
|
|
9
|
+
|
|
10
|
+
// ----- env & globalize
|
|
11
|
+
|
|
12
|
+
export const env = {};
|
|
13
|
+
|
|
14
|
+
export function shim(prefix = 'wq') {
|
|
15
|
+
const apis = [Request, Response, Headers, FormData];
|
|
16
|
+
const descs = [request, response, headers, formData];
|
|
17
|
+
const patch = (api, desc) => {
|
|
18
|
+
const _descs = Object.fromEntries(Object.entries(desc).map(([key, value]) => {
|
|
19
|
+
if (prefix && key in api) {
|
|
20
|
+
key = `${prefix}${key[0].toUpperCase()}${key.slice(1)}`;
|
|
21
|
+
}
|
|
22
|
+
return [key, value];
|
|
23
|
+
}));
|
|
24
|
+
Object.defineProperties(api, _descs);
|
|
25
|
+
};
|
|
26
|
+
for (let i = 0; i < apis.length; i++) {
|
|
27
|
+
const api = apis[i];
|
|
28
|
+
const { prototype, ...statics } = descs[i];
|
|
29
|
+
patch(api, statics);
|
|
30
|
+
if (prototype) {
|
|
31
|
+
patch(api.prototype, prototype);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
globalThis.LiveResponse = LiveResponse;
|
|
35
|
+
globalThis.Observer = Observer;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ----- request
|
|
39
|
+
|
|
40
|
+
const requestOriginals = { prototype: { clone: Request.prototype.clone } };
|
|
41
|
+
|
|
42
|
+
export const request = {
|
|
43
|
+
from: {
|
|
44
|
+
value: function (url, init = {}) {
|
|
45
|
+
if (url instanceof Request) return url;
|
|
46
|
+
let $$type, $$body = init.body;
|
|
47
|
+
if ('body' in init) {
|
|
48
|
+
const { body, headers, $type } = renderHttpMessageInit(init);
|
|
49
|
+
init = { ...init, body, headers };
|
|
50
|
+
$$type = $type;
|
|
51
|
+
}
|
|
52
|
+
const instance = new Request(url, init);
|
|
53
|
+
const responseMeta = _wq(instance, 'meta');
|
|
54
|
+
responseMeta.set('body', $$body);
|
|
55
|
+
responseMeta.set('type', $$type);
|
|
56
|
+
return instance;
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
copy: {
|
|
60
|
+
value: async function (request, init = {}) {
|
|
61
|
+
const attrs = ['method', 'headers', 'mode', 'credentials', 'cache', 'redirect', 'referrer', 'integrity'];
|
|
62
|
+
const requestInit = attrs.reduce(($init, prop) => (
|
|
63
|
+
{
|
|
64
|
+
...$init,
|
|
65
|
+
[prop]: prop in init
|
|
66
|
+
? init[prop]
|
|
67
|
+
: (prop === 'headers'
|
|
68
|
+
? new Headers(request[prop])
|
|
69
|
+
: request[prop])
|
|
70
|
+
}
|
|
71
|
+
), {});
|
|
72
|
+
if (!['GET', 'HEAD'].includes(init.method?.toUpperCase() || request.method)) {
|
|
73
|
+
if ('body' in init) {
|
|
74
|
+
requestInit.body = init.body
|
|
75
|
+
if (!('headers' in init)) {
|
|
76
|
+
requestInit.headers.delete('Content-Type');
|
|
77
|
+
requestInit.headers.delete('Content-Length');
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
requestInit.body = await request.clone().arrayBuffer();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (requestInit.mode === 'navigate') {
|
|
84
|
+
requestInit.mode = 'cors';
|
|
85
|
+
}
|
|
86
|
+
return { url: request.url, ...requestInit };
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
prototype: {
|
|
90
|
+
carries: { get: function () { return new Set(_wq(this, 'meta').get('carries') || []); } },
|
|
91
|
+
parse: { value: async function () { return await parseHttpMessage(this); } },
|
|
92
|
+
clone: {
|
|
93
|
+
value: function (init = {}) {
|
|
94
|
+
const clone = requestOriginals.prototype.clone.call(this, init);
|
|
95
|
+
const requestMeta = _wq(this, 'meta');
|
|
96
|
+
_wq(clone).set('meta', requestMeta);
|
|
97
|
+
return clone;
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// ----- response
|
|
104
|
+
|
|
105
|
+
const responseOriginals = {
|
|
106
|
+
json: Response.json,
|
|
107
|
+
prototype: {
|
|
108
|
+
status: Object.getOwnPropertyDescriptor(Response.prototype, 'status'),
|
|
109
|
+
clone: Response.prototype.clone,
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const response = {
|
|
114
|
+
json: {
|
|
115
|
+
value: function (data, options = {}) {
|
|
116
|
+
const instance = responseOriginals.json(data, options);
|
|
117
|
+
const responseMeta = _wq(instance, 'meta');
|
|
118
|
+
responseMeta.set('body', data);
|
|
119
|
+
responseMeta.set('type', 'json');
|
|
120
|
+
return instance;
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
from: {
|
|
124
|
+
value: function (body, init = {}) {
|
|
125
|
+
if (body instanceof Response) return body;
|
|
126
|
+
let $type, $body = body;
|
|
127
|
+
if (body || body === 0) {
|
|
128
|
+
let headers;
|
|
129
|
+
({ body, headers, $type } = renderHttpMessageInit({ body, headers: init.headers }));
|
|
130
|
+
init = { ...init, headers };
|
|
131
|
+
}
|
|
132
|
+
const instance = new Response(body, init);
|
|
133
|
+
const responseMeta = _wq(instance, 'meta');
|
|
134
|
+
responseMeta.set('body', $body);
|
|
135
|
+
responseMeta.set('type', $type);
|
|
136
|
+
return instance;
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
redirectWith: {
|
|
140
|
+
value: function (url, { status = 302, request = null, response = null }) {
|
|
141
|
+
if (typeof status !== 'string') {
|
|
142
|
+
throw new Error('Redirect code must be an object!');
|
|
143
|
+
}
|
|
144
|
+
if (request && !_isObject(request) || response && !_isObject(response)) {
|
|
145
|
+
throw new Error('Carries (redirect requests and responses) must be an object!');
|
|
146
|
+
}
|
|
147
|
+
const responseInstance = this.redirect(url, status);
|
|
148
|
+
if (request || response) {
|
|
149
|
+
const responseMeta = _wq(responseInstance, 'meta');
|
|
150
|
+
responseMeta.set('carry', { request, response });
|
|
151
|
+
}
|
|
152
|
+
return responseInstance;
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
prototype: {
|
|
156
|
+
status: { get: function () { return _wq(this, 'meta').get('status') || responseOriginals.prototype.status.get.call(this); } },
|
|
157
|
+
carry: { get: function () { return _wq(this, 'meta').get('carry'); } },
|
|
158
|
+
parse: { value: async function () { return await parseHttpMessage(this); } },
|
|
159
|
+
clone: {
|
|
160
|
+
value: function (init = {}) {
|
|
161
|
+
const clone = responseOriginals.prototype.clone.call(this, init);
|
|
162
|
+
const responseMeta = _wq(this, 'meta');
|
|
163
|
+
_wq(clone).set('meta', responseMeta);
|
|
164
|
+
return clone;
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// ----- headers
|
|
171
|
+
|
|
172
|
+
const headersOriginals = {
|
|
173
|
+
set: Headers.prototype.set,
|
|
174
|
+
get: Headers.prototype.get,
|
|
175
|
+
append: Headers.prototype.append,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export const headers = {
|
|
179
|
+
set: {
|
|
180
|
+
value: function (name, value) {
|
|
181
|
+
|
|
182
|
+
// Format "Set-Cookie" response header
|
|
183
|
+
if (/^Set-Cookie$/i.test(name) && _isObject(value)) {
|
|
184
|
+
value = renderCookieObjToString(value);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Format "Cookie" request header
|
|
188
|
+
if (/Cookie/i.test(name) && _isTypeObject(value)) {
|
|
189
|
+
value = [].concat(value).map(renderCookieObjToString).join(';');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Format "Content-Range" response header?
|
|
193
|
+
if (/^Content-Range$/i.test(name) && Array.isArray(value)) {
|
|
194
|
+
if (value.length < 2 || !value[0].includes('-')) {
|
|
195
|
+
throw new Error(`A Content-Range array must be in the format: [ 'start-end', 'total' ]`);
|
|
196
|
+
}
|
|
197
|
+
value = `bytes ${value.join('/')}`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Format "Range" request header?
|
|
201
|
+
if (/^Range$/i.test(name)) {
|
|
202
|
+
let rangeArr = [];
|
|
203
|
+
_arrFrom(value).forEach((range, i) => {
|
|
204
|
+
let rangeStr = Array.isArray(range) ? range.join('-') : range + '';
|
|
205
|
+
if (i === 0 && !rangeStr.includes('bytes=')) {
|
|
206
|
+
rangeStr = `bytes=${rangeStr}`;
|
|
207
|
+
}
|
|
208
|
+
rangeArr.push(rangeStr);
|
|
209
|
+
});
|
|
210
|
+
value = rangeArr.join(', ');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Format "Accept" request header?
|
|
214
|
+
if (/^Accept$/i.test(name) && Array.isArray(value)) {
|
|
215
|
+
value = value.join(',');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return headersOriginals.set.call(this, name, value);
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
append: {
|
|
222
|
+
value: function (name, value) {
|
|
223
|
+
|
|
224
|
+
// Format "Set-Cookie" response header
|
|
225
|
+
if (/^Set-Cookie$/i.test(name) && _isObject(value)) {
|
|
226
|
+
value = renderCookieObjToString(value);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return headersOriginals.append.call(this, name, value);
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
get: {
|
|
233
|
+
value: function (name, parsed = false) {
|
|
234
|
+
let value = headersOriginals.get.call(this, name);
|
|
235
|
+
|
|
236
|
+
// Parse "Set-Cookie" response header
|
|
237
|
+
if (/^Set-Cookie$/i.test(name) && parsed) {
|
|
238
|
+
value = this.getSetCookie()/*IMPORTANT*/.map((str) => {
|
|
239
|
+
const [cookieDefinition, attrsStr] = str.split(';');
|
|
240
|
+
const [name, value] = cookieDefinition.split('=').map((s) => s.trim());
|
|
241
|
+
const cookieObj = { name, value: /*decodeURIComponent*/(value), };
|
|
242
|
+
attrsStr && attrsStr.split(/\;/g).map(attrStr => attrStr.trim().split('=')).forEach(attrsArr => {
|
|
243
|
+
cookieObj[attrsArr[0][0].toLowerCase() + attrsArr[0].substring(1).replace('-', '')] = attrsArr.length === 1 ? true : attrsArr[1];
|
|
244
|
+
});
|
|
245
|
+
return cookieObj;
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Parse "Cookie" request header
|
|
250
|
+
if (/^Cookie$/i.test(name) && parsed) {
|
|
251
|
+
value = value?.split(';').map((str) => {
|
|
252
|
+
const [name, value] = str.split('=').map((s) => s.trim());
|
|
253
|
+
return { name, value: /*decodeURIComponent*/(value), };
|
|
254
|
+
}) || [];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Parse "Content-Range" response header?
|
|
258
|
+
if (/^Content-Range$/i.test(name) && value && parsed) {
|
|
259
|
+
value = _after(value, 'bytes ').split('/');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Parse "Range" request header?
|
|
263
|
+
if (/^Range$/i.test(name) && parsed) {
|
|
264
|
+
value = !value ? [] : _after(value, 'bytes=').split(',').map((rangeStr) => {
|
|
265
|
+
const range = rangeStr.trim().split('-').map((s) => s ? parseInt(s, 10) : null);
|
|
266
|
+
range.render = (totalLength) => {
|
|
267
|
+
if (range[1] === null) {
|
|
268
|
+
range[1] = totalLength - 1;
|
|
269
|
+
}
|
|
270
|
+
if (range[0] === null) {
|
|
271
|
+
range[0] = range[1] ? totalLength - range[1] - 1 : 0;
|
|
272
|
+
}
|
|
273
|
+
return range
|
|
274
|
+
};
|
|
275
|
+
range.isValid = (currentStart, totalLength) => {
|
|
276
|
+
// Start higher than end or vice versa?
|
|
277
|
+
if (range[0] > range[1] || range[1] < range[0]) return false;
|
|
278
|
+
// Stretching beyond valid start/end?
|
|
279
|
+
if (range[0] < currentStart || range[1] > totalLength) return false;
|
|
280
|
+
return true;
|
|
281
|
+
};
|
|
282
|
+
return range;
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Parse "Accept" request header?
|
|
287
|
+
if (/^Accept$/i.test(name) && value && parsed) {
|
|
288
|
+
const parseSpec = (spec) => {
|
|
289
|
+
const [mime, q] = spec.trim().split(';').map((s) => s.trim());
|
|
290
|
+
return [mime, parseFloat((q || 'q=1').replace('q=', ''))];
|
|
291
|
+
};
|
|
292
|
+
const list = value.split(',')
|
|
293
|
+
.map((spec) => parseSpec(spec))
|
|
294
|
+
.sort((a, b) => a[1] > b[1] ? -1 : 1) || [];
|
|
295
|
+
const $value = value;
|
|
296
|
+
value = {
|
|
297
|
+
match(mime) {
|
|
298
|
+
if (!mime) return 0;
|
|
299
|
+
const splitMime = (mime) => mime.split('/').map((s) => s.trim());
|
|
300
|
+
const $mime = splitMime(mime + '');
|
|
301
|
+
return list.reduce((prev, [entry, q]) => {
|
|
302
|
+
if (prev) return prev;
|
|
303
|
+
const $entry = splitMime(entry);
|
|
304
|
+
return [0, 1].every((i) => (($mime[i] === $entry[i]) || $mime[i] === '*' || $entry[i] === '*')) ? q : 0;
|
|
305
|
+
}, 0);
|
|
306
|
+
},
|
|
307
|
+
toString() {
|
|
308
|
+
return $value;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return value;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// ----- formData
|
|
319
|
+
|
|
320
|
+
export const formData = {
|
|
321
|
+
json: { value: createFormDataFromJson },
|
|
322
|
+
prototype: {
|
|
323
|
+
json: {
|
|
324
|
+
value: async function (data = {}) {
|
|
325
|
+
const result = await renderFormDataToJson(this, ...arguments);
|
|
326
|
+
return result;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
// ----- Utils
|
|
333
|
+
|
|
334
|
+
export function renderHttpMessageInit(httpMessageInit) {
|
|
335
|
+
// JSONfy headers
|
|
336
|
+
const headers = (httpMessageInit.headers instanceof Headers) ? [...httpMessageInit.headers.entries()].reduce((_headers, [name, value]) => {
|
|
337
|
+
return { ..._headers, [name/* lower-cased */]: _headers[name] ? [].concat(_headers[name], value) : value };
|
|
338
|
+
}, {}) : Object.keys(httpMessageInit.headers || {}).reduce((_headers, name) => {
|
|
339
|
+
return { ..._headers, [name.toLowerCase()]: httpMessageInit.headers[name] };
|
|
340
|
+
}, {});
|
|
341
|
+
// Process body
|
|
342
|
+
let body = httpMessageInit.body,
|
|
343
|
+
type = dataType(httpMessageInit.body);
|
|
344
|
+
|
|
345
|
+
if (['Blob', 'File'].includes(type)) {
|
|
346
|
+
!headers['content-type'] && (headers['content-type'] = body.type);
|
|
347
|
+
!headers['content-length'] && (headers['content-length'] = body.size);
|
|
348
|
+
} else if (['Uint8Array', 'Uint16Array', 'Uint32Array', 'ArrayBuffer'].includes(type)) {
|
|
349
|
+
!headers['content-length'] && (headers['content-length'] = body.byteLength);
|
|
350
|
+
} else if (type === 'json' && _isTypeObject(body)/*JSON object*/) {
|
|
351
|
+
const [_body, isJsonfiable] = createFormDataFromJson(body, true/*jsonfy*/, true/*getIsJsonfiable*/);
|
|
352
|
+
if (isJsonfiable) {
|
|
353
|
+
body = JSON.stringify(body, (k, v) => v instanceof Error ? { ...v, message: v.message } : v);
|
|
354
|
+
headers['content-type'] = 'application/json';
|
|
355
|
+
headers['content-length'] = (new Blob([body])).size;
|
|
356
|
+
} else {
|
|
357
|
+
body = _body;
|
|
358
|
+
type = 'FormData';
|
|
359
|
+
}
|
|
360
|
+
} else if (type === 'json'/*JSON string*/ && !headers['content-length']) {
|
|
361
|
+
(headers['content-length'] = (body + '').length);
|
|
362
|
+
}
|
|
363
|
+
return { body, headers, $type: type };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export async function parseHttpMessage(httpMessage) {
|
|
367
|
+
let result;
|
|
368
|
+
const contentType = httpMessage.headers.get('Content-Type') || '';
|
|
369
|
+
if (contentType === 'application/x-www-form-urlencoded' || contentType.startsWith('multipart/form-data')) {
|
|
370
|
+
const fd = await httpMessage.formData();
|
|
371
|
+
result = fd && await formData.json.value(fd);
|
|
372
|
+
} else if (contentType.startsWith('application/json')/*can include charset*/) {
|
|
373
|
+
result = await httpMessage.json();
|
|
374
|
+
} else /*if (contentType === 'text/plain')*/ {
|
|
375
|
+
result = httpMessage.body;
|
|
376
|
+
}
|
|
377
|
+
return result;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// -----
|
|
381
|
+
|
|
382
|
+
export function createFormDataFromJson(data = {}, jsonfy = true, getIsJsonfiable = false) {
|
|
383
|
+
const formData = new FormData;
|
|
384
|
+
let isJsonfiable = true;
|
|
385
|
+
DeepURLSearchParams.reduceValue(data, '', (value, contextPath, suggestedKeys = undefined) => {
|
|
386
|
+
if (suggestedKeys) {
|
|
387
|
+
const isJson = dataType(value) === 'json';
|
|
388
|
+
isJsonfiable = isJsonfiable && isJson;
|
|
389
|
+
return isJson && suggestedKeys;
|
|
390
|
+
}
|
|
391
|
+
if (jsonfy && [true, false, null].includes(value)) {
|
|
392
|
+
value = new Blob([value], { type: 'application/json' });
|
|
393
|
+
}
|
|
394
|
+
formData.append(contextPath, value);
|
|
395
|
+
});
|
|
396
|
+
if (getIsJsonfiable) return [formData, isJsonfiable];
|
|
397
|
+
return formData;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export async function renderFormDataToJson(formData, jsonfy = true, getIsJsonfiable = false) {
|
|
401
|
+
let isJsonfiable = true;
|
|
402
|
+
let json;
|
|
403
|
+
for (let [name, value] of formData.entries()) {
|
|
404
|
+
if (!json) { json = _isNumeric(_before(name, '[')) ? [] : {}; }
|
|
405
|
+
let type = dataType(value);
|
|
406
|
+
if (jsonfy && ['Blob', 'File'].includes(type) && value.type === 'application/json') {
|
|
407
|
+
let _value = await value.text();
|
|
408
|
+
value = JSON.parse(_value);
|
|
409
|
+
type = 'json';
|
|
410
|
+
}
|
|
411
|
+
isJsonfiable = isJsonfiable && type === 'json';
|
|
412
|
+
DeepURLSearchParams.set(json, name, value);
|
|
413
|
+
}
|
|
414
|
+
if (getIsJsonfiable) return [json, isJsonfiable];
|
|
415
|
+
return json;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// -----
|
|
419
|
+
|
|
420
|
+
export function renderCookieObjToString(cookieObj) {
|
|
421
|
+
const attrsArr = [`${cookieObj.name}=${/*encodeURIComponent*/(cookieObj.value)}`];
|
|
422
|
+
for (const attrName in cookieObj) {
|
|
423
|
+
if (['name', 'value'].includes(attrName)) continue;
|
|
424
|
+
let _attrName = attrName[0].toUpperCase() + attrName.substring(1);
|
|
425
|
+
if (_attrName === 'MaxAge') { _attrName = 'Max-Age' };
|
|
426
|
+
attrsArr.push(cookieObj[attrName] === true ? _attrName : `${_attrName}=${cookieObj[attrName]}`);
|
|
427
|
+
}
|
|
428
|
+
return attrsArr.join(';');
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ----- shim
|
|
432
|
+
|
|
433
|
+
const importUrl = new URL(import.meta.url);
|
|
434
|
+
if (importUrl.searchParams.has('shim')) {
|
|
435
|
+
shim(importUrl.searchParams.get('shim')?.trim());
|
|
436
|
+
}
|
|
@@ -2,7 +2,7 @@ import { _isObject, _isTypeObject } from '@webqit/util/js/index.js';
|
|
|
2
2
|
import { WQMessagePort, WQMessagePortInstanceTag } from './WQMessagePort.js';
|
|
3
3
|
import { isTypeStream } from '../webflo-fetch/util.js';
|
|
4
4
|
import { WQMessageEvent } from './WQMessageEvent.js';
|
|
5
|
-
import { Observer } from '@webqit/
|
|
5
|
+
import { Observer } from '@webqit/use-live';
|
|
6
6
|
import { _wq } from '../../util.js';
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { _isObject } from '@webqit/util/js/index.js';
|
|
2
|
-
import { renderCookieObjToString } from '../webflo-fetch/
|
|
2
|
+
import { renderCookieObjToString } from '../webflo-fetch/index.js';
|
|
3
3
|
import { HttpState } from './HttpState.js';
|
|
4
4
|
|
|
5
5
|
export class HttpCookies extends HttpState {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { _difference } from '@webqit/util/arr/index.js';
|
|
2
1
|
import { _isObject } from '@webqit/util/js/index.js';
|
|
2
|
+
import { _difference } from '@webqit/util/arr/index.js';
|
|
3
|
+
import { LiveResponse } from '../webflo-fetch/LiveResponse.js';
|
|
3
4
|
import { xURL } from '../webflo-url/xURL.js';
|
|
4
5
|
import { _wq } from '../../util.js';
|
|
5
6
|
|
|
@@ -14,9 +15,9 @@ export class HttpEvent {
|
|
|
14
15
|
#init;
|
|
15
16
|
#abortController = new AbortController;
|
|
16
17
|
|
|
17
|
-
constructor(parentEvent, { request, cookies, session, user,
|
|
18
|
+
constructor(parentEvent, { request, cookies, session, user, client, detail, signal, state, ...rest }) {
|
|
18
19
|
this.#parentEvent = parentEvent;
|
|
19
|
-
this.#init = { request, cookies, session, user,
|
|
20
|
+
this.#init = { request, cookies, session, user, client, detail, signal, state, ...rest };
|
|
20
21
|
this.#url = new xURL(this.#init.request.url);
|
|
21
22
|
this.#parentEvent?.signal.addEventListener('abort', () => this.#abortController.abort(), { once: true });
|
|
22
23
|
this.#init.request.signal?.addEventListener('abort', () => this.#abortController.abort(), { once: true });
|
|
@@ -29,7 +30,7 @@ export class HttpEvent {
|
|
|
29
30
|
|
|
30
31
|
get request() { return this.#init.request; }
|
|
31
32
|
|
|
32
|
-
get
|
|
33
|
+
get client() { return this.#init.client; }
|
|
33
34
|
|
|
34
35
|
get cookies() { return this.#init.cookies; }
|
|
35
36
|
|
|
@@ -37,8 +38,6 @@ export class HttpEvent {
|
|
|
37
38
|
|
|
38
39
|
get user() { return this.#init.user; }
|
|
39
40
|
|
|
40
|
-
get sdk() { return this.#init.sdk; }
|
|
41
|
-
|
|
42
41
|
get detail() { return this.#init.detail; }
|
|
43
42
|
|
|
44
43
|
get signal() { return this.#abortController.signal; }
|
|
@@ -46,6 +45,8 @@ export class HttpEvent {
|
|
|
46
45
|
get state() { return { ...(this.#init.state || {}) }; }
|
|
47
46
|
|
|
48
47
|
#lifecyclePromises = new Set;
|
|
48
|
+
get lifecyclePromises() { return this.#lifecyclePromises; }
|
|
49
|
+
|
|
49
50
|
#lifeCycleResolve;
|
|
50
51
|
#lifeCycleReject;
|
|
51
52
|
#lifeCycleResolutionPromise = new Promise((resolve, reject) => {
|
|
@@ -81,12 +82,12 @@ export class HttpEvent {
|
|
|
81
82
|
&& !this.#lifecyclePromises.size;
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
waitUntil(promise) {
|
|
85
|
-
return this.#extendLifecycle(promise);
|
|
85
|
+
async waitUntil(promise) {
|
|
86
|
+
return await this.#extendLifecycle(promise);
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
waitUntilNavigate() {
|
|
89
|
-
|
|
90
|
+
this.waitUntil(new Promise(() => { }));
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
#internalLiveResponse = new LiveResponse(null, { done: false });
|
|
@@ -101,8 +102,8 @@ export class HttpEvent {
|
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
extend(init = {}) {
|
|
104
|
-
const instance = this.constructor.create(this/*Main difference from clone*/, { ...this.#init, ...init });
|
|
105
|
-
this.#extendLifecycle(instance.lifeCycleComplete(true));
|
|
105
|
+
const instance = this.constructor.create(this/*Main difference from clone*/, { ...this.#init, ...(init || {}) });
|
|
106
|
+
if (init !== false) this.#extendLifecycle(instance.lifeCycleComplete(true));
|
|
106
107
|
return instance;
|
|
107
108
|
}
|
|
108
109
|
|
|
@@ -2,19 +2,19 @@ import { HttpState } from './HttpState.js';
|
|
|
2
2
|
|
|
3
3
|
export class HttpUser extends HttpState {
|
|
4
4
|
|
|
5
|
-
static create({ store, request,
|
|
6
|
-
return new this({ store, request,
|
|
5
|
+
static create({ store, request, client, session }) {
|
|
6
|
+
return new this({ store, request, client, session });
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
#
|
|
9
|
+
#client;
|
|
10
10
|
|
|
11
|
-
constructor({ store, request,
|
|
11
|
+
constructor({ store, request, client, session }) {
|
|
12
12
|
super({
|
|
13
13
|
store,
|
|
14
14
|
request,
|
|
15
15
|
session
|
|
16
16
|
});
|
|
17
|
-
this.#
|
|
17
|
+
this.#client = client;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
async isSignedIn() {
|
|
@@ -34,7 +34,7 @@ export class HttpUser extends HttpState {
|
|
|
34
34
|
|
|
35
35
|
async confirm(data, callback, options = {}) {
|
|
36
36
|
return await new Promise((resolve) => {
|
|
37
|
-
this.#
|
|
37
|
+
this.#client.postRequest(
|
|
38
38
|
data,
|
|
39
39
|
(event) => resolve(callback ? callback(event) : event),
|
|
40
40
|
{ ...options, wqEventOptions: { type: 'confirm' } }
|
|
@@ -44,7 +44,7 @@ export class HttpUser extends HttpState {
|
|
|
44
44
|
|
|
45
45
|
async prompt(data, callback, options = {}) {
|
|
46
46
|
return await new Promise((resolve) => {
|
|
47
|
-
this.#
|
|
47
|
+
this.#client.postRequest(
|
|
48
48
|
data,
|
|
49
49
|
(event) => resolve(callback ? callback(event) : event),
|
|
50
50
|
{ ...options, wqEventOptions: { type: 'prompt' } }
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { State } from '@webqit/quantum-js';
|
|
2
1
|
import { _isFunction, _isArray, _isObject } from '@webqit/util/js/index.js';
|
|
3
2
|
import { _from as _arrFrom } from '@webqit/util/arr/index.js';
|
|
3
|
+
import { LiveResponse } from '../webflo-fetch/LiveResponse.js';
|
|
4
4
|
import { path as Path } from '../webflo-url/util.js';
|
|
5
|
-
import { isGenerator } from '../webflo-fetch/LiveResponse.js';
|
|
6
5
|
|
|
7
6
|
export class WebfloRouter {
|
|
8
7
|
|
|
@@ -170,11 +169,11 @@ export class WebfloRouter {
|
|
|
170
169
|
const returnValue = await handler.call(thisContext, thisTick.event, $next/*next*/, $fetch/*fetch*/);
|
|
171
170
|
|
|
172
171
|
// Handle cleanup on abort
|
|
173
|
-
if (returnValue
|
|
172
|
+
if (LiveResponse.test(returnValue) === 'LiveMode') {
|
|
174
173
|
thisTick.event.signal.addEventListener('abort', () => {
|
|
175
|
-
returnValue.
|
|
174
|
+
returnValue.abort();
|
|
176
175
|
});
|
|
177
|
-
} else if (
|
|
176
|
+
} else if (LiveResponse.test(returnValue) === 'Generator') {
|
|
178
177
|
thisTick.event.signal.addEventListener('abort', () => {
|
|
179
178
|
if (typeof returnValue.return === 'function') {
|
|
180
179
|
returnValue.return();
|
|
@@ -187,13 +186,19 @@ export class WebfloRouter {
|
|
|
187
186
|
resolved = 2;
|
|
188
187
|
resolve(returnValue);
|
|
189
188
|
} else if (typeof returnValue !== 'undefined') {
|
|
190
|
-
|
|
189
|
+
thisTick.event.internalLiveResponse.replaceWith(returnValue, { done: true });
|
|
191
190
|
}
|
|
192
191
|
});
|
|
193
192
|
}
|
|
193
|
+
let returnValue;
|
|
194
194
|
if (_default) {
|
|
195
|
-
|
|
195
|
+
returnValue = await _default.call(thisContext, thisTick.event, remoteFetch);
|
|
196
196
|
}
|
|
197
|
+
try {
|
|
198
|
+
// IMPORTANT: Explicitly terminate the event lifecycle if nothing extends it
|
|
199
|
+
await thisTick.event.waitUntil();
|
|
200
|
+
} catch(e) {}
|
|
201
|
+
return returnValue;
|
|
197
202
|
};
|
|
198
203
|
|
|
199
204
|
return next({
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { headers as headersShim } from '../webflo-fetch/index.js';
|
|
1
2
|
import { HttpCookies } from '../webflo-routing/HttpCookies.js';
|
|
2
3
|
|
|
3
4
|
export class ServerSideCookies extends HttpCookies {
|
|
4
5
|
static create({ request }) {
|
|
6
|
+
const cookies = headersShim.get.value.call(request.headers, 'Cookie', true);
|
|
5
7
|
return new this({
|
|
6
8
|
request,
|
|
7
|
-
entries:
|
|
9
|
+
entries: cookies.map((c) => [c.name, c])
|
|
8
10
|
});
|
|
9
11
|
}
|
|
10
12
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { HttpSession } from '../webflo-routing/HttpSession.js';
|
|
2
|
+
import { headers as headersShim } from '../webflo-fetch/index.js';
|
|
2
3
|
|
|
3
4
|
export class ServerSideSession extends HttpSession {
|
|
4
5
|
|
|
@@ -29,7 +30,7 @@ export class ServerSideSession extends HttpSession {
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
async commit(response = null, devMode = false) {
|
|
32
|
-
if (response && !response.headers
|
|
33
|
+
if (response && !headersShim.get.value.call(response.headers, 'Set-Cookie', true).find((c) => c.name === '__sessid')) {
|
|
33
34
|
// expires six months
|
|
34
35
|
response.headers.append('Set-Cookie', `__sessid=${this.#sessionID}; Path=/; ${!devMode ? 'Secure; ' : ''}HttpOnly; SameSite=Lax${this.#ttl ? `; Max-Age=${this.#ttl}` : ''}`);
|
|
35
36
|
}
|