@wevu/web-apis 1.3.5 → 1.4.0
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 +36 -8
- package/dist/abort.d.mts +2 -4
- package/dist/abort.mjs +2 -3
- package/dist/eventsource-DUCc1iHv.d.mts +82 -0
- package/dist/eventsource-ojngrwBI.mjs +340 -0
- package/dist/eventsource.d.mts +2 -0
- package/dist/eventsource.mjs +2 -0
- package/dist/fetch-CQoQx3mz.mjs +301 -0
- package/dist/fetch-CSESA-oq.d.mts +33 -0
- package/dist/fetch.d.mts +2 -35
- package/dist/fetch.mjs +1 -188
- package/dist/http-DCCqK1QL.mjs +505 -0
- package/dist/{http-CE8-CIHE.d.mts → http-Dd6YCSHK.d.mts} +6 -14
- package/dist/http.d.mts +1 -1
- package/dist/http.mjs +1 -1
- package/dist/index.d.mts +19 -54
- package/dist/index.mjs +35 -104
- package/dist/networkDefaults.d.mts +5 -11
- package/dist/networkDefaults.mjs +1 -2
- package/dist/{shared-85INNxXV.mjs → shared-CARmZrDA.mjs} +5 -9
- package/dist/{shared-Dc42USJC.d.mts → shared-DfU0oEwy.d.mts} +0 -2
- package/dist/shared.d.mts +1 -1
- package/dist/shared.mjs +1 -1
- package/dist/streams-6m8eQfZM.mjs +84 -0
- package/dist/streams.d.mts +31 -0
- package/dist/streams.mjs +1 -0
- package/dist/textCodec-muWByCAW.mjs +132 -0
- package/dist/{url-D0ANYIyk.d.mts → url-CEgyUgm2.d.mts} +0 -2
- package/dist/url-DjMmeEth.mjs +285 -0
- package/dist/url.d.mts +1 -1
- package/dist/url.mjs +1 -229
- package/dist/{web-uZ-hezcd.mjs → web-DDzJNeV3.mjs} +6 -8
- package/dist/{web-DNBBg6y9.d.mts → web-DjLufM4u.d.mts} +1 -7
- package/dist/web.d.mts +1 -1
- package/dist/web.mjs +1 -1
- package/dist/websocket.d.mts +3 -4
- package/dist/websocket.mjs +4 -5
- package/dist/xhr.d.mts +2 -3
- package/dist/xhr.mjs +4 -5
- package/package.json +20 -6
- package/dist/http-mcdF3N9S.mjs +0 -303
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## 1. 简介
|
|
4
4
|
|
|
5
|
-
`@wevu/web-apis` 为小程序运行时提供一组 Web API 兼容层,重点解决第三方库在小程序环境中缺失 `fetch`、`Request`、`Response`、`AbortController`、`XMLHttpRequest`、`URL`、`WebSocket
|
|
5
|
+
`@wevu/web-apis` 为小程序运行时提供一组 Web API 兼容层,重点解决第三方库在小程序环境中缺失 `fetch`、`Request`、`Response`、`AbortController`、`XMLHttpRequest`、`URL`、`WebSocket`、Web Streams、`TextEncoder`、`TextDecoder`、`TextDecoderStream`、`EventSource`、`atob`、`btoa`、`queueMicrotask`、`performance.now`、`crypto.getRandomValues`、`Event`、`CustomEvent` 等全局对象的问题。
|
|
6
6
|
|
|
7
7
|
它主要服务于:
|
|
8
8
|
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
- 按需安装 `fetch`、`Headers`、`Request`、`Response`
|
|
16
16
|
- 提供 `AbortController` / `AbortSignal` 兼容层
|
|
17
17
|
- 提供 `XMLHttpRequest`、`URL`、`URLSearchParams`、`Blob`、`FormData`、`WebSocket` 兼容层
|
|
18
|
+
- 提供 Fetch 响应流、UTF-8 增量解码与标准 GET EventSource;网络增量交付要求宿主提供分块监听和早期真实状态码
|
|
18
19
|
- 提供 `atob`、`btoa`、`queueMicrotask`、`performance.now`、`crypto.getRandomValues`、`Event`、`CustomEvent` 兼容层
|
|
19
20
|
- 自动把能力安装到可用的小程序宿主全局对象
|
|
20
21
|
- 默认基于 `@wevu/api` 的请求能力完成底层转发
|
|
@@ -31,6 +32,7 @@ pnpm add @wevu/web-apis
|
|
|
31
32
|
|
|
32
33
|
```ts
|
|
33
34
|
import { installWebRuntimeGlobals } from '@wevu/web-apis'
|
|
35
|
+
import { fetch } from '@wevu/web-apis/fetch'
|
|
34
36
|
|
|
35
37
|
installWebRuntimeGlobals()
|
|
36
38
|
|
|
@@ -38,14 +40,16 @@ const response = await fetch('https://request-globals.invalid/data')
|
|
|
38
40
|
console.log(await response.json())
|
|
39
41
|
```
|
|
40
42
|
|
|
43
|
+
在 `weapp-vite` 项目中,希望直接使用自由变量 `fetch`、`EventSource` 等 Web 写法时,应配置 `weapp.appPrelude.webRuntime`。编译器会在模块执行前安装构造器并生成局部绑定,避免微信模块包装器中的同名参数遮蔽全局对象;单独调用安装器不能提供这项编译能力。
|
|
44
|
+
|
|
41
45
|
### 4.2 仅安装 Abort 相关能力
|
|
42
46
|
|
|
43
47
|
```ts
|
|
44
48
|
import { installAbortGlobals } from '@wevu/web-apis'
|
|
45
49
|
|
|
46
|
-
installAbortGlobals()
|
|
50
|
+
const runtime = installAbortGlobals()
|
|
47
51
|
|
|
48
|
-
const controller = new AbortController()
|
|
52
|
+
const controller = new runtime.AbortController()
|
|
49
53
|
controller.abort()
|
|
50
54
|
```
|
|
51
55
|
|
|
@@ -64,7 +68,7 @@ installWebRuntimeGlobals({
|
|
|
64
68
|
```ts
|
|
65
69
|
import { installWebRuntimeGlobals } from '@wevu/web-apis'
|
|
66
70
|
|
|
67
|
-
installWebRuntimeGlobals({
|
|
71
|
+
const { btoa, atob, performance, crypto, CustomEvent } = installWebRuntimeGlobals({
|
|
68
72
|
targets: ['atob', 'btoa', 'queueMicrotask', 'performance', 'crypto', 'Event', 'CustomEvent'],
|
|
69
73
|
})
|
|
70
74
|
|
|
@@ -80,7 +84,7 @@ const event = new CustomEvent('payload', { detail: { ok: true } })
|
|
|
80
84
|
```ts
|
|
81
85
|
import { installWebRuntimeGlobals } from '@wevu/web-apis'
|
|
82
86
|
|
|
83
|
-
installWebRuntimeGlobals({
|
|
87
|
+
const { WebSocket } = installWebRuntimeGlobals({
|
|
84
88
|
targets: ['WebSocket'],
|
|
85
89
|
})
|
|
86
90
|
|
|
@@ -109,6 +113,9 @@ socket.onclose = (event) => {
|
|
|
109
113
|
| `fetch` | 基于小程序请求能力适配的 `fetch` 实现 |
|
|
110
114
|
| `HeadersPolyfill` / `RequestPolyfill` / `ResponsePolyfill` | HTTP 相关兼容类 |
|
|
111
115
|
| `TextEncoderPolyfill` / `TextDecoderPolyfill` | 文本编解码兼容类 |
|
|
116
|
+
| `ReadableStreamPolyfill` / `WritableStreamPolyfill` / `TransformStreamPolyfill` | Web Streams 兼容构造器 |
|
|
117
|
+
| `TextDecoderStreamPolyfill` | UTF-8 流式解码器 |
|
|
118
|
+
| `EventSourcePolyfill` | GET SSE 客户端,支持事件监听与自动重连 |
|
|
112
119
|
| `URLPolyfill` / `URLSearchParamsPolyfill` | URL 相关兼容类 |
|
|
113
120
|
| `WebSocketPolyfill` | 基于小程序 `SocketTask` 的 `WebSocket` 兼容实现 |
|
|
114
121
|
| `XMLHttpRequestPolyfill` | XHR 兼容实现 |
|
|
@@ -118,6 +125,7 @@ socket.onclose = (event) => {
|
|
|
118
125
|
- 在小程序环境中运行 `axios` 的 `fetch` 适配器
|
|
119
126
|
- 在小程序环境中运行 `graphql-request`
|
|
120
127
|
- 在小程序环境中直接复用浏览器风格的 `WebSocket` 客户端代码
|
|
128
|
+
- 在具备所需宿主能力的微信环境中复用 Fetch SSE 消费链路或标准 `EventSource`
|
|
121
129
|
- 在小程序环境中运行依赖 `atob` / `btoa` / `queueMicrotask` / `performance.now` / `crypto.getRandomValues` 的工具库
|
|
122
130
|
- 给依赖 `URL` / `FormData` / `Blob` 的库补齐基础全局对象
|
|
123
131
|
|
|
@@ -149,7 +157,7 @@ pnpm --filter @wevu/web-apis typecheck
|
|
|
149
157
|
|
|
150
158
|
## 11. Body 与 Blob 契约
|
|
151
159
|
|
|
152
|
-
`RequestPolyfill` 和 `ResponsePolyfill`
|
|
160
|
+
`RequestPolyfill` 和 `ResponsePolyfill` 提供可读 `body`,以及 `bytes()`、`arrayBuffer()`、`text()`、`json()`、`blob()`。非 null body 只能消费一次;调用消费方法或开始读取、取消 body 流时,`bodyUsed` 会变成 `true`。仅获取 reader 会锁定流,但不改变 `bodyUsed`。body 已消费或锁定时,重复读取和 `clone()` 都会以 `TypeError` 失败。需要多种读取方式时,请在消费前 clone:
|
|
153
161
|
|
|
154
162
|
```ts
|
|
155
163
|
import { ResponsePolyfill } from '@wevu/web-apis'
|
|
@@ -166,11 +174,31 @@ null body 读取为空且保持未消费;空字符串和零长度二进制属
|
|
|
166
174
|
|
|
167
175
|
| 能力 | 支持范围 |
|
|
168
176
|
| ----------------- | ---------------------------------------------------------------------------------------------------------- |
|
|
169
|
-
| Body 读取与 clone |
|
|
177
|
+
| Body 读取与 clone | 缓冲与流式 body 共享单次消费状态,通过 `tee()` 提供 clone 分支,保留错误与取消传播;ponyfill 的 `blob()` 不要求先安装全局 Blob |
|
|
170
178
|
| Blob / File | UTF-8 字节长度、BufferSource 构造快照、独立 `bytes()`、负索引及越界 `slice()`、MIME 规范化 |
|
|
171
179
|
| Headers.forEach | 回调参数 `(value, key, headers)` 和可选 `thisArg` |
|
|
172
180
|
| BlobLike 扩展输入 | 保留异步 `arrayBuffer()` 兼容;无有效 `size` 的自定义对象不保证同步 size/切片语义,也不保证其自身不可变 |
|
|
173
|
-
| Streams |
|
|
181
|
+
| Streams | 支持标准响应流、`getReader()`、`pipeThrough()` 与 `clone()/tee()`;Fetch 网络增量需开启 `enableChunked` 且宿主提供早期真实状态码;未提供流式上传和 `Blob.stream()` |
|
|
174
182
|
| formData() | 未支持响应解析;FormData 请求上传继续支持 multipart 编码 |
|
|
175
183
|
|
|
176
184
|
这些是本包 ponyfill 的能力边界。安装器保留可用的宿主实现,宿主对象的方法完整性应按目标基础库确认。
|
|
185
|
+
|
|
186
|
+
## SSE 与响应流
|
|
187
|
+
|
|
188
|
+
微信端可以保持 Web 消费方式。项目启用 Web Runtime 并设置
|
|
189
|
+
`weapp.appPrelude.webRuntime.networkDefaults.request.enableChunked: true` 后,
|
|
190
|
+
`fetch()` 在收到真实 HTTP 状态码和响应头时返回,后续字节通过 `Response.body` 交付。
|
|
191
|
+
`wevu/fetch` 复用同一实现。普通缓冲请求也提供可读 body,但不表示网络增量交付。
|
|
192
|
+
|
|
193
|
+
`@wevu/web-apis/streams` 导出作为一组选择的 `ReadableStream`、`WritableStream`、`TransformStream`,保留可用的原生实现,否则使用 `web-streams-polyfill@4.3.0` ponyfill。该入口也导出三个 `*Polyfill` 构造器和 `TextDecoderStreamPolyfill`。
|
|
194
|
+
`@wevu/web-apis/eventsource` 导出 `EventSourcePolyfill`、其别名 `EventSource` 与 `MessageEventPolyfill`,也可以由构建器注入全局 `EventSource`。
|
|
195
|
+
EventSource 自动启用分块请求,支持 GET、命名事件、`Last-Event-ID`、`retry` 与自动重连。
|
|
196
|
+
POST、自定义鉴权头使用 Fetch;Fetch 不自动重试请求。
|
|
197
|
+
|
|
198
|
+
缺少 chunk API 或早期 `statusCode` 时,流式 Fetch 以 `NotSupportedError` 失败,
|
|
199
|
+
EventSource 发出 error 并关闭,不推断 HTTP 200。开发者工具和真机能力需要分别验证。
|
|
200
|
+
`withCredentials: true` 不受支持;小程序域名白名单、TLS、超时和后台运行限制仍由宿主管理。
|
|
201
|
+
宿主没有已验证的暂停接收接口,JS 队列不代表网络级反压;慢消费和 clone 分支可能增加内存占用。
|
|
202
|
+
|
|
203
|
+
`AbortSignal` 在 Response 返回后仍能终止读取;`reader.cancel()` 取消请求。
|
|
204
|
+
页面卸载时需清理 EventSource 或中止 Fetch。对 clone 的单个分支取消不会取消另一分支。
|
package/dist/abort.d.mts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { i as RequestGlobalsEventTarget } from "./shared-
|
|
2
|
-
//#region src/abort.d.ts
|
|
1
|
+
import { i as RequestGlobalsEventTarget } from "./shared-DfU0oEwy.mjs";
|
|
3
2
|
export declare class AbortSignalPolyfill extends RequestGlobalsEventTarget {
|
|
4
3
|
aborted: boolean;
|
|
5
4
|
reason: unknown;
|
|
@@ -11,5 +10,4 @@ export declare class AbortSignalPolyfill extends RequestGlobalsEventTarget {
|
|
|
11
10
|
export declare class AbortControllerPolyfill {
|
|
12
11
|
readonly signal: AbortSignalPolyfill;
|
|
13
12
|
abort(reason?: unknown): void;
|
|
14
|
-
}
|
|
15
|
-
//#endregion
|
|
13
|
+
}
|
package/dist/abort.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as RequestGlobalsEventTarget } from "./shared-
|
|
2
|
-
|
|
1
|
+
import { t as RequestGlobalsEventTarget } from "./shared-CARmZrDA.mjs";
|
|
2
|
+
// src/abort.ts
|
|
3
3
|
function createAbortError(reason) {
|
|
4
4
|
if (reason instanceof Error) return reason;
|
|
5
5
|
if (typeof DOMException === "function") return new DOMException("The operation was aborted.", "AbortError");
|
|
@@ -24,5 +24,4 @@ var AbortControllerPolyfill = class {
|
|
|
24
24
|
this.signal.dispatchEvent({ type: "abort" });
|
|
25
25
|
}
|
|
26
26
|
};
|
|
27
|
-
//#endregion
|
|
28
27
|
export { AbortControllerPolyfill, AbortSignalPolyfill };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
interface EventInitPolyfill {
|
|
2
|
+
bubbles?: boolean;
|
|
3
|
+
cancelable?: boolean;
|
|
4
|
+
composed?: boolean;
|
|
5
|
+
}
|
|
6
|
+
interface CustomEventInitPolyfill<T = any> extends EventInitPolyfill {
|
|
7
|
+
detail?: T;
|
|
8
|
+
}
|
|
9
|
+
declare class EventPolyfill {
|
|
10
|
+
readonly bubbles: boolean;
|
|
11
|
+
readonly cancelable: boolean;
|
|
12
|
+
readonly composed: boolean;
|
|
13
|
+
readonly isTrusted = false;
|
|
14
|
+
readonly timeStamp: number;
|
|
15
|
+
readonly type: string;
|
|
16
|
+
currentTarget: unknown;
|
|
17
|
+
target: unknown;
|
|
18
|
+
defaultPrevented: boolean;
|
|
19
|
+
cancelBubble: boolean;
|
|
20
|
+
constructor(type: string, init?: EventInitPolyfill);
|
|
21
|
+
composedPath(): {}[];
|
|
22
|
+
preventDefault(): void;
|
|
23
|
+
stopImmediatePropagation(): void;
|
|
24
|
+
stopPropagation(): void;
|
|
25
|
+
}
|
|
26
|
+
declare class CustomEventPolyfill<T = any> extends EventPolyfill {
|
|
27
|
+
readonly detail: T | null;
|
|
28
|
+
constructor(type: string, init?: CustomEventInitPolyfill<T>);
|
|
29
|
+
}
|
|
30
|
+
/** 区分停止冒泡与停止同一目标上的后续监听。 */
|
|
31
|
+
declare class EventSourceEvent extends EventPolyfill {
|
|
32
|
+
immediatePropagationStopped: boolean;
|
|
33
|
+
stopImmediatePropagation(): void;
|
|
34
|
+
}
|
|
35
|
+
/** 提供事件流消息需要的标准 MessageEvent 字段。 */
|
|
36
|
+
declare class MessageEventPolyfill<T = unknown> extends EventSourceEvent {
|
|
37
|
+
readonly data: T;
|
|
38
|
+
readonly origin: string;
|
|
39
|
+
readonly lastEventId: string;
|
|
40
|
+
readonly source: null;
|
|
41
|
+
readonly ports: MessagePort[];
|
|
42
|
+
constructor(type: string, init?: MessageEventInit<T>);
|
|
43
|
+
}
|
|
44
|
+
/** 事件监听错误独立报告,避免把业务异常当作传输错误并触发重连。 */
|
|
45
|
+
declare class EventSourceEventTarget {
|
|
46
|
+
private readonly listeners;
|
|
47
|
+
addEventListener<K extends keyof EventSourceEventMap>(type: K, callback: ((event: EventSourceEventMap[K]) => void) | null, options?: boolean | AddEventListenerOptions): void;
|
|
48
|
+
addEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions): void;
|
|
49
|
+
removeEventListener<K extends keyof EventSourceEventMap>(type: K, callback: ((event: EventSourceEventMap[K]) => void) | null, options?: boolean | EventListenerOptions): void;
|
|
50
|
+
removeEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: boolean | EventListenerOptions): void;
|
|
51
|
+
dispatchEvent(event: Event): boolean;
|
|
52
|
+
private invoke;
|
|
53
|
+
}
|
|
54
|
+
/** 以标准 Fetch 响应流实现 EventSource;宿主缺失流式元数据时明确失败。 */
|
|
55
|
+
declare class EventSourcePolyfill extends EventSourceEventTarget {
|
|
56
|
+
static readonly CONNECTING = 0;
|
|
57
|
+
static readonly OPEN = 1;
|
|
58
|
+
static readonly CLOSED = 2;
|
|
59
|
+
readonly CONNECTING = 0;
|
|
60
|
+
readonly OPEN = 1;
|
|
61
|
+
readonly CLOSED = 2;
|
|
62
|
+
readonly url: string;
|
|
63
|
+
readonly withCredentials = false;
|
|
64
|
+
readonly [Symbol.toStringTag] = "EventSource";
|
|
65
|
+
onopen: ((this: EventSourcePolyfill, event: Event) => unknown) | null;
|
|
66
|
+
onmessage: ((this: EventSourcePolyfill, event: MessageEvent<string>) => unknown) | null;
|
|
67
|
+
onerror: ((this: EventSourcePolyfill, event: Event) => unknown) | null;
|
|
68
|
+
private state;
|
|
69
|
+
private generation;
|
|
70
|
+
private lastEventId;
|
|
71
|
+
private retryDelay;
|
|
72
|
+
private retryTimer;
|
|
73
|
+
private controller;
|
|
74
|
+
constructor(url: string | URL, init?: EventSourceInit);
|
|
75
|
+
get readyState(): number;
|
|
76
|
+
close(): void;
|
|
77
|
+
private isActive;
|
|
78
|
+
private failConnection;
|
|
79
|
+
private reconnect;
|
|
80
|
+
private connect;
|
|
81
|
+
}
|
|
82
|
+
export { EventPolyfill as i, MessageEventPolyfill as n, CustomEventPolyfill as r, EventSourcePolyfill as t };
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { t as URLPolyfill } from "./url-DjMmeEth.mjs";
|
|
2
|
+
import { AbortControllerPolyfill } from "./abort.mjs";
|
|
3
|
+
import { t as TextDecoderPolyfill } from "./textCodec-muWByCAW.mjs";
|
|
4
|
+
import { t as fetch } from "./fetch-CQoQx3mz.mjs";
|
|
5
|
+
// src/events.ts
|
|
6
|
+
var EventPolyfill = class {
|
|
7
|
+
bubbles;
|
|
8
|
+
cancelable;
|
|
9
|
+
composed;
|
|
10
|
+
isTrusted = false;
|
|
11
|
+
timeStamp = Date.now();
|
|
12
|
+
type;
|
|
13
|
+
currentTarget = null;
|
|
14
|
+
target = null;
|
|
15
|
+
defaultPrevented = false;
|
|
16
|
+
cancelBubble = false;
|
|
17
|
+
constructor(type, init = {}) {
|
|
18
|
+
this.type = String(type);
|
|
19
|
+
this.bubbles = init.bubbles === true;
|
|
20
|
+
this.cancelable = init.cancelable === true;
|
|
21
|
+
this.composed = init.composed === true;
|
|
22
|
+
}
|
|
23
|
+
composedPath() {
|
|
24
|
+
return this.target == null ? [] : [this.target];
|
|
25
|
+
}
|
|
26
|
+
preventDefault() {
|
|
27
|
+
if (this.cancelable) this.defaultPrevented = true;
|
|
28
|
+
}
|
|
29
|
+
stopImmediatePropagation() {
|
|
30
|
+
this.cancelBubble = true;
|
|
31
|
+
}
|
|
32
|
+
stopPropagation() {
|
|
33
|
+
this.cancelBubble = true;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
var CustomEventPolyfill = class extends EventPolyfill {
|
|
37
|
+
detail;
|
|
38
|
+
constructor(type, init = {}) {
|
|
39
|
+
super(type, init);
|
|
40
|
+
this.detail = init.detail ?? null;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
// src/eventsource/events.ts
|
|
44
|
+
/** 区分停止冒泡与停止同一目标上的后续监听。 */
|
|
45
|
+
var EventSourceEvent = class extends EventPolyfill {
|
|
46
|
+
immediatePropagationStopped = false;
|
|
47
|
+
stopImmediatePropagation() {
|
|
48
|
+
super.stopImmediatePropagation();
|
|
49
|
+
this.immediatePropagationStopped = true;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
/** 提供事件流消息需要的标准 MessageEvent 字段。 */
|
|
53
|
+
var MessageEventPolyfill = class extends EventSourceEvent {
|
|
54
|
+
data;
|
|
55
|
+
origin;
|
|
56
|
+
lastEventId;
|
|
57
|
+
source = null;
|
|
58
|
+
ports = [];
|
|
59
|
+
constructor(type, init = {}) {
|
|
60
|
+
super(type, init);
|
|
61
|
+
this.data = init.data;
|
|
62
|
+
this.origin = init.origin ?? "";
|
|
63
|
+
this.lastEventId = init.lastEventId ?? "";
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
function reportListenerError(error) {
|
|
67
|
+
const host = globalThis;
|
|
68
|
+
if (typeof host.reportError === "function") host.reportError(error);
|
|
69
|
+
else setTimeout(() => {
|
|
70
|
+
throw error;
|
|
71
|
+
}, 0);
|
|
72
|
+
}
|
|
73
|
+
/** 事件监听错误独立报告,避免把业务异常当作传输错误并触发重连。 */
|
|
74
|
+
var EventSourceEventTarget = class {
|
|
75
|
+
listeners = /* @__PURE__ */ new Map();
|
|
76
|
+
addEventListener(type, callback, options = false) {
|
|
77
|
+
if (!callback) return;
|
|
78
|
+
const capture = typeof options === "boolean" ? options : options.capture === true;
|
|
79
|
+
const signal = typeof options === "boolean" ? void 0 : options.signal;
|
|
80
|
+
const listeners = this.listeners.get(type) ?? [];
|
|
81
|
+
if (signal?.aborted || listeners.some((entry) => entry.callback === callback && entry.capture === capture)) return;
|
|
82
|
+
const entry = {
|
|
83
|
+
callback,
|
|
84
|
+
capture,
|
|
85
|
+
once: typeof options === "object" && options.once === true,
|
|
86
|
+
signal
|
|
87
|
+
};
|
|
88
|
+
if (signal) {
|
|
89
|
+
entry.onAbort = () => this.removeEventListener(type, callback, capture);
|
|
90
|
+
signal.addEventListener("abort", entry.onAbort, { once: true });
|
|
91
|
+
}
|
|
92
|
+
listeners.push(entry);
|
|
93
|
+
this.listeners.set(type, listeners);
|
|
94
|
+
}
|
|
95
|
+
removeEventListener(type, callback, options = false) {
|
|
96
|
+
const capture = typeof options === "boolean" ? options : options.capture === true;
|
|
97
|
+
const listeners = this.listeners.get(type);
|
|
98
|
+
const index = listeners?.findIndex((entry) => entry.callback === callback && entry.capture === capture) ?? -1;
|
|
99
|
+
if (listeners && index !== -1) {
|
|
100
|
+
const [entry] = listeners.splice(index, 1);
|
|
101
|
+
if (entry?.onAbort) entry.signal?.removeEventListener("abort", entry.onAbort);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
dispatchEvent(event) {
|
|
105
|
+
const payload = event;
|
|
106
|
+
Object.defineProperties(payload, {
|
|
107
|
+
target: {
|
|
108
|
+
configurable: true,
|
|
109
|
+
writable: true,
|
|
110
|
+
value: this
|
|
111
|
+
},
|
|
112
|
+
currentTarget: {
|
|
113
|
+
configurable: true,
|
|
114
|
+
writable: true,
|
|
115
|
+
value: this
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
for (const entry of [...this.listeners.get(event.type) ?? []]) {
|
|
119
|
+
if (!this.listeners.get(event.type)?.includes(entry)) continue;
|
|
120
|
+
if (entry.once) this.removeEventListener(event.type, entry.callback, entry.capture);
|
|
121
|
+
this.invoke(() => {
|
|
122
|
+
if (typeof entry.callback === "function") entry.callback.call(this, event);
|
|
123
|
+
else entry.callback.handleEvent(event);
|
|
124
|
+
});
|
|
125
|
+
if (payload.immediatePropagationStopped) break;
|
|
126
|
+
}
|
|
127
|
+
if (!payload.immediatePropagationStopped) {
|
|
128
|
+
const handler = this[`on${event.type}`];
|
|
129
|
+
if (typeof handler === "function") this.invoke(() => handler.call(this, event));
|
|
130
|
+
}
|
|
131
|
+
payload.currentTarget = null;
|
|
132
|
+
return !event.defaultPrevented;
|
|
133
|
+
}
|
|
134
|
+
invoke(callback) {
|
|
135
|
+
try {
|
|
136
|
+
callback();
|
|
137
|
+
} catch (error) {
|
|
138
|
+
reportListenerError(error);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
// src/eventsource/parser.ts
|
|
143
|
+
/** 增量解析事件流;仅空行提交事件,连接结束时丢弃未完成的数据。 */
|
|
144
|
+
var EventStreamParser = class {
|
|
145
|
+
callbacks;
|
|
146
|
+
fragments = [];
|
|
147
|
+
data = [];
|
|
148
|
+
eventType = "";
|
|
149
|
+
lastEventId;
|
|
150
|
+
firstCharacter = true;
|
|
151
|
+
skipLf = false;
|
|
152
|
+
constructor(callbacks, lastEventId = "") {
|
|
153
|
+
this.callbacks = callbacks;
|
|
154
|
+
this.lastEventId = lastEventId;
|
|
155
|
+
}
|
|
156
|
+
feed(chunk) {
|
|
157
|
+
let start = 0;
|
|
158
|
+
if (this.firstCharacter && chunk.length > 0) {
|
|
159
|
+
this.firstCharacter = false;
|
|
160
|
+
if (chunk.charCodeAt(0) === 65279) start = 1;
|
|
161
|
+
}
|
|
162
|
+
for (let index = start; index < chunk.length; index++) {
|
|
163
|
+
const code = chunk.charCodeAt(index);
|
|
164
|
+
if (this.skipLf) {
|
|
165
|
+
this.skipLf = false;
|
|
166
|
+
if (code === 10) {
|
|
167
|
+
start = index + 1;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (code !== 10 && code !== 13) continue;
|
|
172
|
+
this.fragments.push(chunk.slice(start, index));
|
|
173
|
+
const line = this.fragments.join("");
|
|
174
|
+
this.fragments = [];
|
|
175
|
+
start = index + 1;
|
|
176
|
+
this.skipLf = code === 13;
|
|
177
|
+
this.line(line);
|
|
178
|
+
}
|
|
179
|
+
if (start < chunk.length) this.fragments.push(chunk.slice(start));
|
|
180
|
+
}
|
|
181
|
+
line(line) {
|
|
182
|
+
if (line === "") {
|
|
183
|
+
this.callbacks.onId(this.lastEventId);
|
|
184
|
+
const event = this.data.length > 0 ? {
|
|
185
|
+
type: this.eventType || "message",
|
|
186
|
+
data: this.data.join("\n"),
|
|
187
|
+
lastEventId: this.lastEventId
|
|
188
|
+
} : null;
|
|
189
|
+
this.data = [];
|
|
190
|
+
this.eventType = "";
|
|
191
|
+
if (event) this.callbacks.onEvent(event);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (line.startsWith(":")) return;
|
|
195
|
+
const separator = line.indexOf(":");
|
|
196
|
+
const field = separator === -1 ? line : line.slice(0, separator);
|
|
197
|
+
let value = separator === -1 ? "" : line.slice(separator + 1);
|
|
198
|
+
if (value.startsWith(" ")) value = value.slice(1);
|
|
199
|
+
switch (field) {
|
|
200
|
+
case "data":
|
|
201
|
+
this.data.push(value);
|
|
202
|
+
break;
|
|
203
|
+
case "event":
|
|
204
|
+
this.eventType = value;
|
|
205
|
+
break;
|
|
206
|
+
case "id":
|
|
207
|
+
if (!value.includes("\0")) this.lastEventId = value;
|
|
208
|
+
break;
|
|
209
|
+
case "retry": if (/^\d+$/.test(value)) {
|
|
210
|
+
const delay = Number(value);
|
|
211
|
+
if (Number.isFinite(delay)) this.callbacks.onRetry(Math.min(delay, 2147483647));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
// src/eventsource.ts
|
|
217
|
+
function notSupported(message) {
|
|
218
|
+
const error = new Error(message);
|
|
219
|
+
error.name = "NotSupportedError";
|
|
220
|
+
return error;
|
|
221
|
+
}
|
|
222
|
+
function event(type) {
|
|
223
|
+
return new EventSourceEvent(type);
|
|
224
|
+
}
|
|
225
|
+
/** 以标准 Fetch 响应流实现 EventSource;宿主缺失流式元数据时明确失败。 */
|
|
226
|
+
var EventSourcePolyfill = class EventSourcePolyfill extends EventSourceEventTarget {
|
|
227
|
+
static CONNECTING = 0;
|
|
228
|
+
static OPEN = 1;
|
|
229
|
+
static CLOSED = 2;
|
|
230
|
+
CONNECTING = 0;
|
|
231
|
+
OPEN = 1;
|
|
232
|
+
CLOSED = 2;
|
|
233
|
+
url;
|
|
234
|
+
withCredentials = false;
|
|
235
|
+
[Symbol.toStringTag] = "EventSource";
|
|
236
|
+
onopen = null;
|
|
237
|
+
onmessage = null;
|
|
238
|
+
onerror = null;
|
|
239
|
+
state = EventSourcePolyfill.CONNECTING;
|
|
240
|
+
generation = 0;
|
|
241
|
+
lastEventId = "";
|
|
242
|
+
retryDelay = 3e3;
|
|
243
|
+
retryTimer;
|
|
244
|
+
controller;
|
|
245
|
+
constructor(url, init = {}) {
|
|
246
|
+
super();
|
|
247
|
+
if (init.withCredentials) throw notSupported("EventSource withCredentials is not supported by this mini-program transport");
|
|
248
|
+
const base = globalThis.location?.href;
|
|
249
|
+
this.url = new URLPolyfill(String(url), base).href;
|
|
250
|
+
Promise.resolve().then(() => this.connect());
|
|
251
|
+
}
|
|
252
|
+
get readyState() {
|
|
253
|
+
return this.state;
|
|
254
|
+
}
|
|
255
|
+
close() {
|
|
256
|
+
this.state = EventSourcePolyfill.CLOSED;
|
|
257
|
+
this.generation++;
|
|
258
|
+
clearTimeout(this.retryTimer);
|
|
259
|
+
this.retryTimer = void 0;
|
|
260
|
+
this.controller?.abort();
|
|
261
|
+
this.controller = void 0;
|
|
262
|
+
}
|
|
263
|
+
isActive(generation) {
|
|
264
|
+
return this.generation === generation && this.state !== EventSourcePolyfill.CLOSED;
|
|
265
|
+
}
|
|
266
|
+
failConnection() {
|
|
267
|
+
this.close();
|
|
268
|
+
this.dispatchEvent(event("error"));
|
|
269
|
+
}
|
|
270
|
+
reconnect(generation) {
|
|
271
|
+
if (!this.isActive(generation)) return;
|
|
272
|
+
this.state = EventSourcePolyfill.CONNECTING;
|
|
273
|
+
this.dispatchEvent(event("error"));
|
|
274
|
+
if (this.isActive(generation)) this.retryTimer = setTimeout(() => {
|
|
275
|
+
this.retryTimer = void 0;
|
|
276
|
+
this.connect();
|
|
277
|
+
}, this.retryDelay);
|
|
278
|
+
}
|
|
279
|
+
async connect() {
|
|
280
|
+
if (this.state === EventSourcePolyfill.CLOSED) return;
|
|
281
|
+
const generation = ++this.generation;
|
|
282
|
+
const controller = new AbortControllerPolyfill();
|
|
283
|
+
this.controller = controller;
|
|
284
|
+
let reader;
|
|
285
|
+
try {
|
|
286
|
+
const headers = { Accept: "text/event-stream" };
|
|
287
|
+
if (this.lastEventId) headers["Last-Event-ID"] = this.lastEventId;
|
|
288
|
+
const response = await fetch(this.url, {
|
|
289
|
+
headers,
|
|
290
|
+
signal: controller.signal,
|
|
291
|
+
miniProgram: { enableChunked: true }
|
|
292
|
+
});
|
|
293
|
+
if (!this.isActive(generation)) {
|
|
294
|
+
await response.body?.cancel();
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const contentType = response.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase();
|
|
298
|
+
if (response.status !== 200 || contentType !== "text/event-stream" || !response.body) {
|
|
299
|
+
this.failConnection();
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const origin = new URLPolyfill(response.url || this.url).origin;
|
|
303
|
+
const parser = new EventStreamParser({
|
|
304
|
+
onEvent: (message) => {
|
|
305
|
+
if (this.isActive(generation)) this.dispatchEvent(new MessageEventPolyfill(message.type, {
|
|
306
|
+
...message,
|
|
307
|
+
origin
|
|
308
|
+
}));
|
|
309
|
+
},
|
|
310
|
+
onId: (id) => {
|
|
311
|
+
this.lastEventId = id;
|
|
312
|
+
},
|
|
313
|
+
onRetry: (delay) => {
|
|
314
|
+
this.retryDelay = delay;
|
|
315
|
+
}
|
|
316
|
+
}, this.lastEventId);
|
|
317
|
+
const decoder = new TextDecoderPolyfill("utf-8", { ignoreBOM: true });
|
|
318
|
+
reader = response.body.getReader();
|
|
319
|
+
this.state = EventSourcePolyfill.OPEN;
|
|
320
|
+
this.dispatchEvent(event("open"));
|
|
321
|
+
while (this.isActive(generation)) {
|
|
322
|
+
const chunk = await reader.read();
|
|
323
|
+
if (chunk.done) {
|
|
324
|
+
parser.feed(decoder.decode());
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
parser.feed(decoder.decode(chunk.value, { stream: true }));
|
|
328
|
+
}
|
|
329
|
+
this.reconnect(generation);
|
|
330
|
+
} catch (error) {
|
|
331
|
+
if (!this.isActive(generation)) return;
|
|
332
|
+
if (error instanceof Error && error.name === "NotSupportedError") this.failConnection();
|
|
333
|
+
else this.reconnect(generation);
|
|
334
|
+
} finally {
|
|
335
|
+
reader?.releaseLock();
|
|
336
|
+
if (this.controller === controller) this.controller = void 0;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
export { EventPolyfill as i, MessageEventPolyfill as n, CustomEventPolyfill as r, EventSourcePolyfill as t };
|