stream.ts 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. // using tauri command to send request
  2. // see src-tauri/src/stream.rs, and src-tauri/src/main.rs
  3. // 1. invoke('stream_fetch', {url, method, headers, body}), get response with headers.
  4. // 2. listen event: `stream-response` multi times to get body
  5. type ResponseEvent = {
  6. id: number;
  7. payload: {
  8. request_id: number;
  9. status?: number;
  10. chunk?: number[];
  11. };
  12. };
  13. type StreamResponse = {
  14. request_id: number;
  15. status: number;
  16. status_text: string;
  17. headers: Record<string, string>;
  18. };
  19. export function fetch(url: string, options?: RequestInit): Promise<any> {
  20. if (window.__TAURI__) {
  21. const {
  22. signal,
  23. method = "GET",
  24. headers: _headers = {},
  25. body = [],
  26. } = options || {};
  27. let unlisten: Function | undefined;
  28. let request_id = 0;
  29. const ts = new TransformStream();
  30. const writer = ts.writable.getWriter();
  31. let closed = false;
  32. const close = () => {
  33. if (closed) return;
  34. closed = true;
  35. unlisten && unlisten();
  36. writer.ready.then(() => {
  37. writer.close().catch((e) => console.error(e));
  38. });
  39. };
  40. if (signal) {
  41. signal.addEventListener("abort", () => close());
  42. }
  43. // @ts-ignore 2. listen response multi times, and write to Response.body
  44. window.__TAURI__.event
  45. .listen("stream-response", (e: ResponseEvent) => {
  46. const { request_id: rid, chunk, status } = e?.payload || {};
  47. if (request_id != rid) {
  48. return;
  49. }
  50. if (chunk) {
  51. writer.ready.then(() => {
  52. writer.write(new Uint8Array(chunk));
  53. });
  54. } else if (status === 0) {
  55. // end of body
  56. close();
  57. }
  58. })
  59. .then((u: Function) => (unlisten = u));
  60. const headers: Record<string, string> = {
  61. Accept: "application/json, text/plain, */*",
  62. "Accept-Language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7",
  63. "User-Agent": navigator.userAgent,
  64. };
  65. for (const item of new Headers(_headers || {})) {
  66. headers[item[0]] = item[1];
  67. }
  68. return window.__TAURI__
  69. .invoke("stream_fetch", {
  70. method: method.toUpperCase(),
  71. url,
  72. headers,
  73. // TODO FormData
  74. body:
  75. typeof body === "string"
  76. ? Array.from(new TextEncoder().encode(body))
  77. : [],
  78. })
  79. .then((res: StreamResponse) => {
  80. request_id = res.request_id;
  81. const { status, status_text: statusText, headers } = res;
  82. const response = new Response(ts.readable, {
  83. status,
  84. statusText,
  85. headers,
  86. });
  87. if (status >= 300) {
  88. setTimeout(close, 100);
  89. }
  90. return response;
  91. })
  92. .catch((e) => {
  93. console.error("stream error", e);
  94. throw e;
  95. });
  96. }
  97. return window.fetch(url, options);
  98. }