@webqit/fetch-plus 0.1.2 → 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 +23 -15
- package/src/HeadersPlus.js +128 -56
- package/src/LiveResponse.js +233 -155
- package/src/RequestPlus.js +9 -7
- package/src/ResponsePlus.js +6 -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,7 +1,7 @@
|
|
|
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
|
|
|
@@ -10,47 +10,55 @@ export class FormDataPlus extends FormData {
|
|
|
10
10
|
return Object.setPrototypeOf(formData, FormDataPlus.prototype);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
static json(data = {}, {
|
|
13
|
+
static json(data = {}, { encodeLiterals = true, meta = false } = {}) {
|
|
14
14
|
const formData = new FormDataPlus;
|
|
15
|
-
let
|
|
15
|
+
let isDirectlySerializable = true;
|
|
16
16
|
|
|
17
17
|
URLSearchParamsPlus.reduceValue(data, '', (value, contextPath, suggestedKeys = undefined) => {
|
|
18
18
|
if (suggestedKeys) {
|
|
19
19
|
const isJson = dataType(value) === 'json';
|
|
20
|
-
|
|
20
|
+
isDirectlySerializable = isDirectlySerializable && isJson;
|
|
21
21
|
return isJson && suggestedKeys;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
if (
|
|
24
|
+
if (encodeLiterals && [true, false, null].includes(value)) {
|
|
25
25
|
value = new Blob([value + ''], { type: 'application/json' });
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
formData.append(contextPath, value);
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
-
if (
|
|
31
|
+
if (meta) return { result: formData, isDirectlySerializable };
|
|
32
32
|
return formData;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
async json({
|
|
36
|
-
let
|
|
35
|
+
async json({ decodeLiterals = true, meta = false } = {}) {
|
|
36
|
+
let isDirectlySerializable = true;
|
|
37
37
|
let json;
|
|
38
38
|
|
|
39
39
|
for (let [name, value] of this.entries()) {
|
|
40
40
|
if (!json) json = _isNumeric(_before(name, '[')) ? [] : {};
|
|
41
41
|
|
|
42
42
|
let type = dataType(value);
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
value
|
|
46
|
-
|
|
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
|
+
}
|
|
47
52
|
}
|
|
48
53
|
|
|
49
|
-
|
|
54
|
+
isDirectlySerializable = isDirectlySerializable && type === 'json';
|
|
55
|
+
if (/^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/.test(value)) {
|
|
56
|
+
value = Number(value);
|
|
57
|
+
}
|
|
50
58
|
URLSearchParamsPlus.set(json, name, value);
|
|
51
59
|
}
|
|
52
60
|
|
|
53
|
-
if (
|
|
61
|
+
if (meta) return { result: json, isDirectlySerializable };
|
|
54
62
|
return json;
|
|
55
63
|
}
|
|
56
64
|
}
|
package/src/HeadersPlus.js
CHANGED
|
@@ -2,7 +2,7 @@ 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;
|
|
@@ -11,39 +11,35 @@ export class HeadersPlus extends upgradeMixin(Headers) {
|
|
|
11
11
|
|
|
12
12
|
set(name, value) {
|
|
13
13
|
// Format "Set-Cookie" response header
|
|
14
|
-
if (/^Set-Cookie$/i.test(name)
|
|
15
|
-
|
|
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
|
+
}
|
|
16
23
|
}
|
|
17
24
|
|
|
18
25
|
// Format "Cookie" request header
|
|
19
|
-
if (/Cookie/i.test(name)
|
|
20
|
-
value =
|
|
26
|
+
if (/Cookie/i.test(name)) {
|
|
27
|
+
value = renderCookieInput(value);
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
// Format "Content-Range" response header?
|
|
24
|
-
if (/^Content-Range$/i.test(name)
|
|
25
|
-
|
|
26
|
-
throw new Error(`A Content-Range array must be in the format: [ 'start-end', 'total' ]`);
|
|
27
|
-
}
|
|
28
|
-
value = `bytes ${value.join('/')}`;
|
|
31
|
+
if (/^Content-Range$/i.test(name)) {
|
|
32
|
+
value = renderContentRangeInput(value);
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
// Format "Range" request header?
|
|
32
36
|
if (/^Range$/i.test(name)) {
|
|
33
|
-
|
|
34
|
-
_arrFrom(value).forEach((range, i) => {
|
|
35
|
-
let rangeStr = Array.isArray(range) ? range.join('-') : range + '';
|
|
36
|
-
if (i === 0 && !rangeStr.includes('bytes=')) {
|
|
37
|
-
rangeStr = `bytes=${rangeStr}`;
|
|
38
|
-
}
|
|
39
|
-
rangeArr.push(rangeStr);
|
|
40
|
-
});
|
|
41
|
-
value = rangeArr.join(', ');
|
|
37
|
+
value = renderRangeInput(value);
|
|
42
38
|
}
|
|
43
39
|
|
|
44
40
|
// Format "Accept" request header?
|
|
45
|
-
if (/^Accept$/i.test(name)
|
|
46
|
-
value = value
|
|
41
|
+
if (/^Accept$/i.test(name)) {
|
|
42
|
+
value = renderAcceptInput(value);
|
|
47
43
|
}
|
|
48
44
|
|
|
49
45
|
return super.set(name, value);
|
|
@@ -51,22 +47,49 @@ export class HeadersPlus extends upgradeMixin(Headers) {
|
|
|
51
47
|
|
|
52
48
|
append(name, value) {
|
|
53
49
|
// Format "Set-Cookie" response header
|
|
54
|
-
if (/^Set-Cookie$/i.test(name)
|
|
55
|
-
|
|
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);
|
|
56
78
|
}
|
|
79
|
+
|
|
57
80
|
return super.append(name, value);
|
|
58
81
|
}
|
|
59
82
|
|
|
60
|
-
get(name,
|
|
83
|
+
get(name, structured = false) {
|
|
61
84
|
let value = super.get(name);
|
|
62
85
|
|
|
63
86
|
// Parse "Set-Cookie" response header
|
|
64
|
-
if (/^Set-Cookie$/i.test(name) &&
|
|
87
|
+
if (/^Set-Cookie$/i.test(name) && structured) {
|
|
65
88
|
value = this.getSetCookie()/*IMPORTANT*/.map((str) => {
|
|
66
|
-
const [cookieDefinition,
|
|
89
|
+
const [cookieDefinition, ...attrs] = str.split(';');
|
|
67
90
|
const [name, value] = cookieDefinition.split('=').map((s) => s.trim());
|
|
68
91
|
const cookieObj = { name, value: /*decodeURIComponent*/(value), };
|
|
69
|
-
|
|
92
|
+
attrs.map((attrStr) => attrStr.trim().split('=')).forEach(attrsArr => {
|
|
70
93
|
cookieObj[attrsArr[0][0].toLowerCase() + attrsArr[0].substring(1).replace('-', '')] = attrsArr.length === 1 ? true : attrsArr[1];
|
|
71
94
|
});
|
|
72
95
|
return cookieObj;
|
|
@@ -74,7 +97,7 @@ export class HeadersPlus extends upgradeMixin(Headers) {
|
|
|
74
97
|
}
|
|
75
98
|
|
|
76
99
|
// Parse "Cookie" request header
|
|
77
|
-
if (/^Cookie$/i.test(name) &&
|
|
100
|
+
if (/^Cookie$/i.test(name) && structured) {
|
|
78
101
|
value = value?.split(';').map((str) => {
|
|
79
102
|
const [name, value] = str.split('=').map((s) => s.trim());
|
|
80
103
|
return { name, value: /*decodeURIComponent*/(value), };
|
|
@@ -82,58 +105,66 @@ export class HeadersPlus extends upgradeMixin(Headers) {
|
|
|
82
105
|
}
|
|
83
106
|
|
|
84
107
|
// Parse "Content-Range" response header?
|
|
85
|
-
if (/^Content-Range$/i.test(name) && value &&
|
|
108
|
+
if (/^Content-Range$/i.test(name) && value && structured) {
|
|
86
109
|
value = _after(value, 'bytes ').split('/');
|
|
87
110
|
}
|
|
88
111
|
|
|
89
112
|
// Parse "Range" request header?
|
|
90
|
-
if (/^Range$/i.test(name) &&
|
|
113
|
+
if (/^Range$/i.test(name) && structured) {
|
|
91
114
|
value = !value ? [] : _after(value, 'bytes=').split(',').map((rangeStr) => {
|
|
92
115
|
const range = rangeStr.trim().split('-').map((s) => s ? parseInt(s, 10) : null);
|
|
93
|
-
range.
|
|
94
|
-
|
|
95
|
-
|
|
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;
|
|
96
122
|
}
|
|
97
|
-
if (
|
|
98
|
-
|
|
123
|
+
if (offsets[0] === null) {
|
|
124
|
+
offsets[0] = offsets[1] ? totalLength - offsets[1] - 1 : 0;
|
|
99
125
|
}
|
|
100
|
-
return
|
|
126
|
+
return offsets;
|
|
101
127
|
};
|
|
102
|
-
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
|
+
];
|
|
103
133
|
// Start higher than end or vice versa?
|
|
104
|
-
if (
|
|
134
|
+
if (offsets[0] > offsets[1]) return false;
|
|
105
135
|
// Stretching beyond valid start/end?
|
|
106
|
-
if (
|
|
136
|
+
if (offsets[0] < currentStart || offsets[1] >= totalLength) return false;
|
|
107
137
|
return true;
|
|
108
138
|
};
|
|
139
|
+
range.toString = () => {
|
|
140
|
+
return rangeStr;
|
|
141
|
+
};
|
|
109
142
|
return range;
|
|
110
143
|
});
|
|
111
144
|
}
|
|
112
145
|
|
|
113
146
|
// Parse "Accept" request header?
|
|
114
|
-
if (/^Accept$/i.test(name) && value &&
|
|
147
|
+
if (/^Accept$/i.test(name) && value && structured) {
|
|
115
148
|
const parseSpec = (spec) => {
|
|
116
149
|
const [mime, q] = spec.trim().split(';').map((s) => s.trim());
|
|
117
150
|
return [mime, parseFloat((q || 'q=1').replace('q=', ''))];
|
|
118
151
|
};
|
|
119
|
-
const
|
|
152
|
+
const $value = value;
|
|
153
|
+
value = value.split(',')
|
|
120
154
|
.map((spec) => parseSpec(spec))
|
|
121
155
|
.sort((a, b) => a[1] > b[1] ? -1 : 1) || [];
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
toString() {
|
|
135
|
-
return $value;
|
|
136
|
-
}
|
|
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;
|
|
137
168
|
};
|
|
138
169
|
}
|
|
139
170
|
|
|
@@ -151,3 +182,44 @@ export function renderCookieObjToString(cookieObj) {
|
|
|
151
182
|
}
|
|
152
183
|
return attrsArr.join('; ');
|
|
153
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
|
+
|