@webqit/fetch-plus 0.1.1 → 0.1.3
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/README.md +2050 -2
- package/dist/main.js +1 -1
- package/dist/main.js.map +4 -4
- package/package.json +8 -8
- package/src/FormDataPlus.js +25 -16
- package/src/HeadersPlus.js +130 -57
- package/src/LiveResponse.js +232 -154
- package/src/RequestPlus.js +11 -7
- package/src/ResponsePlus.js +8 -6
- package/src/index.js +0 -1
- package/src/messageParserMixin.js +217 -0
- package/test/1.basic.test.js +314 -0
- package/test/2.LiveResponse.test.js +261 -0
- package/test/3.LiveResponse.integration.test.js +459 -0
- package/src/URLSearchParamsPlus.js +0 -80
- package/src/core.js +0 -172
- package/test/basic.test.js +0 -0
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
],
|
|
12
12
|
"homepage": "https://fetch-plus.netlify.app/",
|
|
13
13
|
"icon": "https://webqit.io/icon.svg",
|
|
14
|
-
"version": "0.1.
|
|
14
|
+
"version": "0.1.3",
|
|
15
15
|
"license": "MIT",
|
|
16
16
|
"repository": {
|
|
17
17
|
"type": "git",
|
|
@@ -21,9 +21,7 @@
|
|
|
21
21
|
"url": "https://github.com/webqit/fetch-plus/issues"
|
|
22
22
|
},
|
|
23
23
|
"type": "module",
|
|
24
|
-
"
|
|
25
|
-
".": "./src/index.js"
|
|
26
|
-
},
|
|
24
|
+
"main": "./src/index.js",
|
|
27
25
|
"scripts": {
|
|
28
26
|
"test": "mocha --extension .test.js --recursive --timeout 5000 --exit",
|
|
29
27
|
"build": "esbuild main=src/index.browser.js --bundle --minify --sourcemap --outdir=dist",
|
|
@@ -35,12 +33,14 @@
|
|
|
35
33
|
"@webqit/util": "^0.8.16"
|
|
36
34
|
},
|
|
37
35
|
"peerDependencies": {
|
|
38
|
-
"@webqit/observer": "^3.8.
|
|
39
|
-
"@webqit/port-plus": "^0.1.
|
|
36
|
+
"@webqit/observer": "^3.8.17",
|
|
37
|
+
"@webqit/port-plus": "^0.1.9",
|
|
38
|
+
"@webqit/url-plus": "^0.1.3"
|
|
40
39
|
},
|
|
41
40
|
"devDependencies": {
|
|
42
|
-
"@webqit/observer": "^3.8.
|
|
43
|
-
"@webqit/port-plus": "^0.1.
|
|
41
|
+
"@webqit/observer": "^3.8.17",
|
|
42
|
+
"@webqit/port-plus": "^0.1.9",
|
|
43
|
+
"@webqit/url-plus": "^0.1.3",
|
|
44
44
|
"chai": "^4.3.4",
|
|
45
45
|
"chai-as-promised": "^7.1.1",
|
|
46
46
|
"esbuild": "^0.20.2",
|
package/src/FormDataPlus.js
CHANGED
|
@@ -1,55 +1,64 @@
|
|
|
1
1
|
import { _before } from '@webqit/util/str/index.js';
|
|
2
2
|
import { _isNumeric } from '@webqit/util/js/index.js';
|
|
3
|
-
import { URLSearchParamsPlus } from '
|
|
4
|
-
import { dataType, _meta, _wq } from './
|
|
3
|
+
import { URLSearchParamsPlus } from '@webqit/url-plus';
|
|
4
|
+
import { dataType, _meta, _wq } from './messageParserMixin.js';
|
|
5
5
|
|
|
6
6
|
export class FormDataPlus extends FormData {
|
|
7
7
|
|
|
8
8
|
static upgradeInPlace(formData) {
|
|
9
|
-
|
|
9
|
+
if (formData instanceof FormDataPlus) return formData;
|
|
10
|
+
return Object.setPrototypeOf(formData, FormDataPlus.prototype);
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
static json(data = {}, {
|
|
13
|
+
static json(data = {}, { encodeLiterals = true, meta = false } = {}) {
|
|
13
14
|
const formData = new FormDataPlus;
|
|
14
|
-
let
|
|
15
|
+
let isDirectlySerializable = true;
|
|
15
16
|
|
|
16
17
|
URLSearchParamsPlus.reduceValue(data, '', (value, contextPath, suggestedKeys = undefined) => {
|
|
17
18
|
if (suggestedKeys) {
|
|
18
19
|
const isJson = dataType(value) === 'json';
|
|
19
|
-
|
|
20
|
+
isDirectlySerializable = isDirectlySerializable && isJson;
|
|
20
21
|
return isJson && suggestedKeys;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
if (
|
|
24
|
+
if (encodeLiterals && [true, false, null].includes(value)) {
|
|
24
25
|
value = new Blob([value + ''], { type: 'application/json' });
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
formData.append(contextPath, value);
|
|
28
29
|
});
|
|
29
30
|
|
|
30
|
-
if (
|
|
31
|
+
if (meta) return { result: formData, isDirectlySerializable };
|
|
31
32
|
return formData;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
async json({
|
|
35
|
-
let
|
|
35
|
+
async json({ decodeLiterals = true, meta = false } = {}) {
|
|
36
|
+
let isDirectlySerializable = true;
|
|
36
37
|
let json;
|
|
37
38
|
|
|
38
39
|
for (let [name, value] of this.entries()) {
|
|
39
40
|
if (!json) json = _isNumeric(_before(name, '[')) ? [] : {};
|
|
40
41
|
|
|
41
42
|
let type = dataType(value);
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
value
|
|
45
|
-
|
|
43
|
+
if (decodeLiterals
|
|
44
|
+
&& ['Blob', 'File'].includes(type)
|
|
45
|
+
&& value.type === 'application/json'
|
|
46
|
+
&& [4, 5].includes(value.size)) {
|
|
47
|
+
let _value = JSON.parse(await value.text());
|
|
48
|
+
if ([null, true, false].includes(_value)) {
|
|
49
|
+
value = _value;
|
|
50
|
+
type = 'json';
|
|
51
|
+
}
|
|
46
52
|
}
|
|
47
53
|
|
|
48
|
-
|
|
54
|
+
isDirectlySerializable = isDirectlySerializable && type === 'json';
|
|
55
|
+
if (/^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/.test(value)) {
|
|
56
|
+
value = Number(value);
|
|
57
|
+
}
|
|
49
58
|
URLSearchParamsPlus.set(json, name, value);
|
|
50
59
|
}
|
|
51
60
|
|
|
52
|
-
if (
|
|
61
|
+
if (meta) return { result: json, isDirectlySerializable };
|
|
53
62
|
return json;
|
|
54
63
|
}
|
|
55
64
|
}
|
package/src/HeadersPlus.js
CHANGED
|
@@ -2,47 +2,44 @@ import { _isObject, _isTypeObject } from '@webqit/util/js/index.js';
|
|
|
2
2
|
import { _from as _arrFrom } from '@webqit/util/arr/index.js';
|
|
3
3
|
import { _after } from '@webqit/util/str/index.js';
|
|
4
4
|
|
|
5
|
-
export class HeadersPlus extends
|
|
5
|
+
export class HeadersPlus extends Headers {
|
|
6
6
|
|
|
7
7
|
static upgradeInPlace(headers) {
|
|
8
|
-
|
|
8
|
+
if (headers instanceof HeadersPlus) return headers;
|
|
9
|
+
return Object.setPrototypeOf(headers, HeadersPlus.prototype);
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
set(name, value) {
|
|
12
13
|
// Format "Set-Cookie" response header
|
|
13
|
-
if (/^Set-Cookie$/i.test(name)
|
|
14
|
-
|
|
14
|
+
if (/^Set-Cookie$/i.test(name)) {
|
|
15
|
+
if (Array.isArray(value)) {
|
|
16
|
+
this.delete(name); // IMPORTANT
|
|
17
|
+
for (const v of value) this.append(name, v);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (_isObject(value)) {
|
|
21
|
+
value = renderCookieObjToString(value);
|
|
22
|
+
}
|
|
15
23
|
}
|
|
16
24
|
|
|
17
25
|
// Format "Cookie" request header
|
|
18
|
-
if (/Cookie/i.test(name)
|
|
19
|
-
value =
|
|
26
|
+
if (/Cookie/i.test(name)) {
|
|
27
|
+
value = renderCookieInput(value);
|
|
20
28
|
}
|
|
21
29
|
|
|
22
30
|
// Format "Content-Range" response header?
|
|
23
|
-
if (/^Content-Range$/i.test(name)
|
|
24
|
-
|
|
25
|
-
throw new Error(`A Content-Range array must be in the format: [ 'start-end', 'total' ]`);
|
|
26
|
-
}
|
|
27
|
-
value = `bytes ${value.join('/')}`;
|
|
31
|
+
if (/^Content-Range$/i.test(name)) {
|
|
32
|
+
value = renderContentRangeInput(value);
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
// Format "Range" request header?
|
|
31
36
|
if (/^Range$/i.test(name)) {
|
|
32
|
-
|
|
33
|
-
_arrFrom(value).forEach((range, i) => {
|
|
34
|
-
let rangeStr = Array.isArray(range) ? range.join('-') : range + '';
|
|
35
|
-
if (i === 0 && !rangeStr.includes('bytes=')) {
|
|
36
|
-
rangeStr = `bytes=${rangeStr}`;
|
|
37
|
-
}
|
|
38
|
-
rangeArr.push(rangeStr);
|
|
39
|
-
});
|
|
40
|
-
value = rangeArr.join(', ');
|
|
37
|
+
value = renderRangeInput(value);
|
|
41
38
|
}
|
|
42
39
|
|
|
43
40
|
// Format "Accept" request header?
|
|
44
|
-
if (/^Accept$/i.test(name)
|
|
45
|
-
value = value
|
|
41
|
+
if (/^Accept$/i.test(name)) {
|
|
42
|
+
value = renderAcceptInput(value);
|
|
46
43
|
}
|
|
47
44
|
|
|
48
45
|
return super.set(name, value);
|
|
@@ -50,22 +47,49 @@ export class HeadersPlus extends upgradeMixin(Headers) {
|
|
|
50
47
|
|
|
51
48
|
append(name, value) {
|
|
52
49
|
// Format "Set-Cookie" response header
|
|
53
|
-
if (/^Set-Cookie$/i.test(name)
|
|
54
|
-
|
|
50
|
+
if (/^Set-Cookie$/i.test(name)) {
|
|
51
|
+
if (Array.isArray(value)) {
|
|
52
|
+
for (const v of value) this.append(name, v);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (_isObject(value)) {
|
|
56
|
+
value = renderCookieObjToString(value);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Format "Cookie" request header
|
|
61
|
+
if (/Cookie/i.test(name)) {
|
|
62
|
+
value = renderCookieInput(value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Format "Content-Range" response header?
|
|
66
|
+
if (/^Content-Range$/i.test(name)) {
|
|
67
|
+
value = renderContentRangeInput(value);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Format "Range" request header?
|
|
71
|
+
if (/^Range$/i.test(name)) {
|
|
72
|
+
value = renderRangeInput(value);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Format "Accept" request header?
|
|
76
|
+
if (/^Accept$/i.test(name)) {
|
|
77
|
+
value = renderAcceptInput(value);
|
|
55
78
|
}
|
|
79
|
+
|
|
56
80
|
return super.append(name, value);
|
|
57
81
|
}
|
|
58
82
|
|
|
59
|
-
get(name,
|
|
83
|
+
get(name, structured = false) {
|
|
60
84
|
let value = super.get(name);
|
|
61
85
|
|
|
62
86
|
// Parse "Set-Cookie" response header
|
|
63
|
-
if (/^Set-Cookie$/i.test(name) &&
|
|
87
|
+
if (/^Set-Cookie$/i.test(name) && structured) {
|
|
64
88
|
value = this.getSetCookie()/*IMPORTANT*/.map((str) => {
|
|
65
|
-
const [cookieDefinition,
|
|
89
|
+
const [cookieDefinition, ...attrs] = str.split(';');
|
|
66
90
|
const [name, value] = cookieDefinition.split('=').map((s) => s.trim());
|
|
67
91
|
const cookieObj = { name, value: /*decodeURIComponent*/(value), };
|
|
68
|
-
|
|
92
|
+
attrs.map((attrStr) => attrStr.trim().split('=')).forEach(attrsArr => {
|
|
69
93
|
cookieObj[attrsArr[0][0].toLowerCase() + attrsArr[0].substring(1).replace('-', '')] = attrsArr.length === 1 ? true : attrsArr[1];
|
|
70
94
|
});
|
|
71
95
|
return cookieObj;
|
|
@@ -73,7 +97,7 @@ export class HeadersPlus extends upgradeMixin(Headers) {
|
|
|
73
97
|
}
|
|
74
98
|
|
|
75
99
|
// Parse "Cookie" request header
|
|
76
|
-
if (/^Cookie$/i.test(name) &&
|
|
100
|
+
if (/^Cookie$/i.test(name) && structured) {
|
|
77
101
|
value = value?.split(';').map((str) => {
|
|
78
102
|
const [name, value] = str.split('=').map((s) => s.trim());
|
|
79
103
|
return { name, value: /*decodeURIComponent*/(value), };
|
|
@@ -81,58 +105,66 @@ export class HeadersPlus extends upgradeMixin(Headers) {
|
|
|
81
105
|
}
|
|
82
106
|
|
|
83
107
|
// Parse "Content-Range" response header?
|
|
84
|
-
if (/^Content-Range$/i.test(name) && value &&
|
|
108
|
+
if (/^Content-Range$/i.test(name) && value && structured) {
|
|
85
109
|
value = _after(value, 'bytes ').split('/');
|
|
86
110
|
}
|
|
87
111
|
|
|
88
112
|
// Parse "Range" request header?
|
|
89
|
-
if (/^Range$/i.test(name) &&
|
|
113
|
+
if (/^Range$/i.test(name) && structured) {
|
|
90
114
|
value = !value ? [] : _after(value, 'bytes=').split(',').map((rangeStr) => {
|
|
91
115
|
const range = rangeStr.trim().split('-').map((s) => s ? parseInt(s, 10) : null);
|
|
92
|
-
range.
|
|
93
|
-
|
|
94
|
-
|
|
116
|
+
range.resolveAgainst = (totalLength) => {
|
|
117
|
+
const offsets = [...range];
|
|
118
|
+
if (offsets[1] === null) {
|
|
119
|
+
offsets[1] = totalLength - 1;
|
|
120
|
+
} else {
|
|
121
|
+
offsets[1] = Math.min(offsets[1], totalLength) - 1;
|
|
95
122
|
}
|
|
96
|
-
if (
|
|
97
|
-
|
|
123
|
+
if (offsets[0] === null) {
|
|
124
|
+
offsets[0] = offsets[1] ? totalLength - offsets[1] - 1 : 0;
|
|
98
125
|
}
|
|
99
|
-
return
|
|
126
|
+
return offsets;
|
|
100
127
|
};
|
|
101
|
-
range.
|
|
128
|
+
range.canResolveAgainst = (currentStart, totalLength) => {
|
|
129
|
+
const offsets = [
|
|
130
|
+
typeof range[0] === 'number' ? range[0] : currentStart,
|
|
131
|
+
typeof range[1] === 'number' ? range[1] : totalLength - 1
|
|
132
|
+
];
|
|
102
133
|
// Start higher than end or vice versa?
|
|
103
|
-
if (
|
|
134
|
+
if (offsets[0] > offsets[1]) return false;
|
|
104
135
|
// Stretching beyond valid start/end?
|
|
105
|
-
if (
|
|
136
|
+
if (offsets[0] < currentStart || offsets[1] >= totalLength) return false;
|
|
106
137
|
return true;
|
|
107
138
|
};
|
|
139
|
+
range.toString = () => {
|
|
140
|
+
return rangeStr;
|
|
141
|
+
};
|
|
108
142
|
return range;
|
|
109
143
|
});
|
|
110
144
|
}
|
|
111
145
|
|
|
112
146
|
// Parse "Accept" request header?
|
|
113
|
-
if (/^Accept$/i.test(name) && value &&
|
|
147
|
+
if (/^Accept$/i.test(name) && value && structured) {
|
|
114
148
|
const parseSpec = (spec) => {
|
|
115
149
|
const [mime, q] = spec.trim().split(';').map((s) => s.trim());
|
|
116
150
|
return [mime, parseFloat((q || 'q=1').replace('q=', ''))];
|
|
117
151
|
};
|
|
118
|
-
const
|
|
152
|
+
const $value = value;
|
|
153
|
+
value = value.split(',')
|
|
119
154
|
.map((spec) => parseSpec(spec))
|
|
120
155
|
.sort((a, b) => a[1] > b[1] ? -1 : 1) || [];
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
toString() {
|
|
134
|
-
return $value;
|
|
135
|
-
}
|
|
156
|
+
value.match = (mime) => {
|
|
157
|
+
if (!mime) return 0;
|
|
158
|
+
const splitMime = (mime) => mime.split('/').map((s) => s.trim());
|
|
159
|
+
const $mime = splitMime(mime + '');
|
|
160
|
+
return value.reduce((prev, [entry, q]) => {
|
|
161
|
+
if (prev) return prev;
|
|
162
|
+
const $entry = splitMime(entry);
|
|
163
|
+
return [0, 1].every((i) => (($mime[i] === $entry[i]) || $mime[i] === '*' || $entry[i] === '*')) ? q : 0;
|
|
164
|
+
}, 0);
|
|
165
|
+
};
|
|
166
|
+
value.toString = () => {
|
|
167
|
+
return $value;
|
|
136
168
|
};
|
|
137
169
|
}
|
|
138
170
|
|
|
@@ -150,3 +182,44 @@ export function renderCookieObjToString(cookieObj) {
|
|
|
150
182
|
}
|
|
151
183
|
return attrsArr.join('; ');
|
|
152
184
|
}
|
|
185
|
+
|
|
186
|
+
function renderCookieInput(value) {
|
|
187
|
+
if (_isTypeObject(value)) {
|
|
188
|
+
value = [].concat(value).map(renderCookieObjToString).join('; ');
|
|
189
|
+
}
|
|
190
|
+
return value;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function renderRangeInput(value) {
|
|
194
|
+
let rangeArr = [];
|
|
195
|
+
_arrFrom(value).forEach((range, i) => {
|
|
196
|
+
let rangeStr = Array.isArray(range) ? range.map((n) => [null, undefined].includes(n) ? '' : n).join('-') : range + '';
|
|
197
|
+
if (i === 0 && !rangeStr.includes('bytes=')) {
|
|
198
|
+
rangeStr = `bytes=${rangeStr}`;
|
|
199
|
+
}
|
|
200
|
+
rangeArr.push(rangeStr);
|
|
201
|
+
});
|
|
202
|
+
return rangeArr.join(', ');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function renderContentRangeInput(value) {
|
|
206
|
+
if (Array.isArray(value)) {
|
|
207
|
+
if (value.length < 2 || !value[0].includes('-')) {
|
|
208
|
+
throw new Error(`A Content-Range array must be in the format: [ 'start-end', 'total' ]`);
|
|
209
|
+
}
|
|
210
|
+
value = `bytes ${value.join('/')}`;
|
|
211
|
+
}
|
|
212
|
+
return value;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function renderAcceptInput(value) {
|
|
216
|
+
if (Array.isArray(value)) {
|
|
217
|
+
value = value.map(
|
|
218
|
+
(s) => Array.isArray(s) ? s.map(
|
|
219
|
+
(s, i) => i === 1 && (s = parseFloat(s), true) ? (s === 1 ? '' : `;q=${s}`) : s.trim()
|
|
220
|
+
).join('') : s.trim()
|
|
221
|
+
).join(',');
|
|
222
|
+
}
|
|
223
|
+
return value;
|
|
224
|
+
}
|
|
225
|
+
|