stream.ts 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. // using tauri command to send request
  2. // Stream utilities for web version
  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<Response> {
  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 setRequestId: Function | undefined;
  29. const requestIdPromise = new Promise((resolve) => (setRequestId = resolve));
  30. const ts = new TransformStream();
  31. const writer = ts.writable.getWriter();
  32. let closed = false;
  33. const close = () => {
  34. if (closed) return;
  35. closed = true;
  36. unlisten && unlisten();
  37. writer.ready.then(() => {
  38. writer.close().catch((e) => console.error(e));
  39. });
  40. };
  41. if (signal) {
  42. signal.addEventListener("abort", () => close());
  43. }
  44. // @ts-ignore 2. listen response multi times, and write to Response.body
  45. window.__TAURI__.event
  46. .listen("stream-response", (e: ResponseEvent) =>
  47. requestIdPromise.then((request_id) => {
  48. const { request_id: rid, chunk, status } = e?.payload || {};
  49. if (request_id != rid) {
  50. return;
  51. }
  52. if (chunk) {
  53. writer.ready.then(() => {
  54. writer.write(new Uint8Array(chunk));
  55. });
  56. } else if (status === 0) {
  57. // end of body
  58. close();
  59. }
  60. }),
  61. )
  62. .then((u: Function) => (unlisten = u));
  63. const headers: Record<string, string> = {
  64. Accept: "application/json, text/plain, */*",
  65. "Accept-Language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7",
  66. "User-Agent": navigator.userAgent,
  67. };
  68. for (const item of new Headers(_headers || {})) {
  69. headers[item[0]] = item[1];
  70. }
  71. return window.__TAURI__
  72. .invoke("stream_fetch", {
  73. method: method.toUpperCase(),
  74. url,
  75. headers,
  76. // TODO FormData
  77. body:
  78. typeof body === "string"
  79. ? Array.from(new TextEncoder().encode(body))
  80. : [],
  81. })
  82. .then((res: StreamResponse) => {
  83. const { request_id, status, status_text: statusText, headers } = res;
  84. setRequestId?.(request_id);
  85. const response = new Response(ts.readable, {
  86. status,
  87. statusText,
  88. headers,
  89. });
  90. if (status >= 300) {
  91. setTimeout(close, 100);
  92. }
  93. return response;
  94. })
  95. .catch((e) => {
  96. console.error("stream error", e);
  97. // throw e;
  98. return new Response("", { status: 599 });
  99. });
  100. }
  101. return window.fetch(url, options);
  102. }