GM_fetch
2023-03-19
2023-02-23
await request.text()するか、init?.bodyから取り出すしかない
2023-02-06
fetchもこのstateでresolveするらしいので、こちらのほうがよりfetchに近い promise は、サーバがヘッダを応答するとすぐに組み込みの Response クラスのオブジェクトで resolve します。
規格で決まっている挙動かは不明
MDNで同様の説明は見つからなかった
09:28:52 書き換え終了
変更するところはほとんどなかった
2023-02-03 09:37:54 もうあったんかい!
まあ、書き方が古いのでどのみち書き直すことになっただろうが
takker.iconが考慮していないケースにも対応しているみたいなので、参考にする
code:user.js
import { GM_fetch } from "./mod.ts";
unsafeWindow.GM_fetch = GM_fetch;
code:mod.ts
/// <reference no-default-lib="true" />
/// <reference lib="esnext" />
/// <reference lib="dom" />
/// <reference lib="dom.iterable" />
const parseHeaders = (rawHeaders: string) =>
new Headers(
rawHeaders
// Replace instances of \r\n and \n followed by at least one space or horizontal tab with a space
.replace(/\r?\n\t +/g, " ") .split(/\r\n|\r|\n/)
.flatMap((header) => {
const key, value = header.split(":").map((text) => text.trim()); if (!key) return [];
})
);
/** CORSを突破してfetchする
*
* interfaceはfetchと同一
*/
export const GM_fetch = (input: RequestInfo | URL, init?: RequestInit): Promise<Response> =>
new Promise((resolve, reject) => {
const request = new Request(input, init);
const headers = Object.fromEntries(request.headers.entries());
if (init?.referrer) {
headers.Referer = init.referrer;
}
if (request.signal?.aborted) {
reject(new DOMException("Aborted", "AbortError"));
return;
}
const { abort } = GM_xmlhttpRequest({
// @ts-ignore 多分変な文字列は入ってこないはず
method: request.method,
url: request.url,
headers,
// await request.text() でも同じことができるが、二度手間なのでinitから直接取り出す
...(init?.body ? ({ data: init.body }) : ({})),
anonymous: request.credentials === "omit",
// @ts-ignore streamも指定可能
responseType: "stream",
onerror: () => {
reject(new TypeError("Network request failed"));
},
ontimeout: () => {
reject(new TypeError("Network request timeout"));
},
onabort: () => {
reject(new DOMException("Aborted", "AbortError"));
},
// fetchはHEADERS_RECEIVEDの段階でresolveするので、それにあわせる
onreadystatechange: (res) => {
switch (res.readyState) {
case 2: {
const response = new Response(res.response, {
status: res.status,
statusText: res.statusText,
headers: parseHeaders(res.responseHeaders),
});
// urlはconstructor経由で設定できない
Object.defineProperty(response, "url", { value: res.finalUrl });
resolve(response);
break;
}
case 4:
request.signal?.removeEventListener?.("abort", abort);
break;
default:
break;
}
},
});
request.signal?.addEventListener?.("abort", abort);
});
scrapboxで動くサンプル
code:scrapbox.user.js
// ==UserScript==
// @name GM_fetch
// @version 0.1.7
// @description An implementation of the fetch API which leverages GM_xmlhttpRequest
// @author takker
// @connect *
// @grant GM_xmlhttpRequest
// @license MIT
// @copyright Copyright (c) 2023 takker
// ==/UserScript==
var u=i=>new Headers(i.replace(/\r?\n\t +/g," ").split(/\r\n|\r|\n/).flatMap(n=>{letr,t=n.split(":").map(e=>e.trim());return r?r,t:[]})),l=(i,n)=>new Promise((r,t)=>{let e=new Request(i,n),s=Object.fromEntries(e.headers.entries());if(n?.referrer&&(s.Referer=n.referrer),e.signal?.aborted){t(new DOMException("Aborted","AbortError"));return}let{abort:a}=GM_xmlhttpRequest({method:e.method,url:e.url,headers:s,...n?.body?{data:n.body}:{},anonymous:e.credentials==="omit",responseType:"stream",onerror:()=>{t(new TypeError("Network request failed"))},ontimeout:()=>{t(new TypeError("Network request timeout"))},onabort:()=>{t(new DOMException("Aborted","AbortError"))},onreadystatechange:o=>{switch(o.readyState){case 2:{let d=new Response(o.response,{status:o.status,statusText:o.statusText,headers:u(o.responseHeaders)});Object.defineProperty(d,"url",{value:o.finalUrl}),r(d);break}case 4:e.signal?.removeEventListener?.("abort",a);break;default:break}}});e.signal?.addEventListener?.("abort",a)});unsafeWindow.GM_fetch=l;