@nsnanocat/util 2.6.4 → 2.6.7
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/package.json +1 -1
- package/polyfill/Storage.mjs +73 -4
- package/polyfill/fetch.mjs +129 -7
package/package.json
CHANGED
package/polyfill/Storage.mjs
CHANGED
|
@@ -102,7 +102,9 @@ export class Storage {
|
|
|
102
102
|
keyValue = Storage.data[keyName];
|
|
103
103
|
break;
|
|
104
104
|
case "Node.js":
|
|
105
|
-
|
|
105
|
+
Storage.data = Storage.#loaddata(Storage.dataFile);
|
|
106
|
+
keyValue = Storage.data?.[keyName];
|
|
107
|
+
break;
|
|
106
108
|
default:
|
|
107
109
|
keyValue = Storage.data?.[keyName] || null;
|
|
108
110
|
break;
|
|
@@ -163,7 +165,11 @@ export class Storage {
|
|
|
163
165
|
result = true;
|
|
164
166
|
break;
|
|
165
167
|
case "Node.js":
|
|
166
|
-
|
|
168
|
+
Storage.data = Storage.#loaddata(Storage.dataFile);
|
|
169
|
+
Storage.data[keyName] = keyValue;
|
|
170
|
+
Storage.#writedata(Storage.dataFile);
|
|
171
|
+
result = true;
|
|
172
|
+
break;
|
|
167
173
|
default:
|
|
168
174
|
result = Storage.data?.[keyName] || null;
|
|
169
175
|
break;
|
|
@@ -218,7 +224,12 @@ export class Storage {
|
|
|
218
224
|
result = true;
|
|
219
225
|
break;
|
|
220
226
|
case "Node.js":
|
|
221
|
-
|
|
227
|
+
// result = false;
|
|
228
|
+
Storage.data = Storage.#loaddata(Storage.dataFile);
|
|
229
|
+
delete Storage.data[keyName];
|
|
230
|
+
Storage.#writedata(Storage.dataFile);
|
|
231
|
+
result = true;
|
|
232
|
+
break;
|
|
222
233
|
default:
|
|
223
234
|
result = false;
|
|
224
235
|
break;
|
|
@@ -252,7 +263,12 @@ export class Storage {
|
|
|
252
263
|
result = true;
|
|
253
264
|
break;
|
|
254
265
|
case "Node.js":
|
|
255
|
-
|
|
266
|
+
// result = false;
|
|
267
|
+
Storage.data = Storage.#loaddata(Storage.dataFile);
|
|
268
|
+
Storage.data = {};
|
|
269
|
+
Storage.#writedata(Storage.dataFile);
|
|
270
|
+
result = true;
|
|
271
|
+
break;
|
|
256
272
|
default:
|
|
257
273
|
result = false;
|
|
258
274
|
break;
|
|
@@ -260,4 +276,57 @@ export class Storage {
|
|
|
260
276
|
return result;
|
|
261
277
|
}
|
|
262
278
|
|
|
279
|
+
/**
|
|
280
|
+
* 从 Node.js 数据文件加载 JSON。
|
|
281
|
+
* Load JSON data from Node.js data file.
|
|
282
|
+
*
|
|
283
|
+
* @private
|
|
284
|
+
* @param {string} dataFile 数据文件名 / Data file name.
|
|
285
|
+
* @returns {Record<string, any>}
|
|
286
|
+
*/
|
|
287
|
+
static #loaddata = dataFile => {
|
|
288
|
+
if ($app === "Node.js") {
|
|
289
|
+
this.fs = this.fs ? this.fs : require("fs");
|
|
290
|
+
this.path = this.path ? this.path : require("path");
|
|
291
|
+
const curDirDataFilePath = this.path.resolve(dataFile);
|
|
292
|
+
const rootDirDataFilePath = this.path.resolve(process.cwd(), dataFile);
|
|
293
|
+
const isCurDirDataFile = this.fs.existsSync(curDirDataFilePath);
|
|
294
|
+
const isRootDirDataFile = !isCurDirDataFile && this.fs.existsSync(rootDirDataFilePath);
|
|
295
|
+
if (isCurDirDataFile || isRootDirDataFile) {
|
|
296
|
+
const datPath = isCurDirDataFile ? curDirDataFilePath : rootDirDataFilePath;
|
|
297
|
+
try {
|
|
298
|
+
return JSON.parse(this.fs.readFileSync(datPath));
|
|
299
|
+
} catch (e) {
|
|
300
|
+
return {};
|
|
301
|
+
}
|
|
302
|
+
} else return {};
|
|
303
|
+
} else return {};
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* 将内存数据写入 Node.js 数据文件。
|
|
308
|
+
* Persist in-memory data to Node.js data file.
|
|
309
|
+
*
|
|
310
|
+
* @private
|
|
311
|
+
* @param {string} [dataFile=this.dataFile] 数据文件名 / Data file name.
|
|
312
|
+
* @returns {void}
|
|
313
|
+
*/
|
|
314
|
+
static #writedata = (dataFile = this.dataFile) => {
|
|
315
|
+
if ($app === "Node.js") {
|
|
316
|
+
this.fs = this.fs ? this.fs : require("fs");
|
|
317
|
+
this.path = this.path ? this.path : require("path");
|
|
318
|
+
const curDirDataFilePath = this.path.resolve(dataFile);
|
|
319
|
+
const rootDirDataFilePath = this.path.resolve(process.cwd(), dataFile);
|
|
320
|
+
const isCurDirDataFile = this.fs.existsSync(curDirDataFilePath);
|
|
321
|
+
const isRootDirDataFile = !isCurDirDataFile && this.fs.existsSync(rootDirDataFilePath);
|
|
322
|
+
const jsondata = JSON.stringify(this.data);
|
|
323
|
+
if (isCurDirDataFile) {
|
|
324
|
+
this.fs.writeFileSync(curDirDataFilePath, jsondata);
|
|
325
|
+
} else if (isRootDirDataFile) {
|
|
326
|
+
this.fs.writeFileSync(rootDirDataFilePath, jsondata);
|
|
327
|
+
} else {
|
|
328
|
+
this.fs.writeFileSync(curDirDataFilePath, jsondata);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
};
|
|
263
332
|
}
|
package/polyfill/fetch.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import { StatusTexts } from "./StatusTexts.mjs";
|
|
|
16
16
|
* @property {string} [policy] 指定策略 / Preferred policy.
|
|
17
17
|
* @property {boolean} [redirection] 是否跟随重定向 / Whether to follow redirects.
|
|
18
18
|
* @property {boolean} ["auto-redirect"] 平台重定向字段 / Platform redirect flag.
|
|
19
|
+
* @property {boolean|number|string} ["auto-cookie"] Worker / Node.js Cookie 开关 / Worker / Node.js Cookie toggle.
|
|
19
20
|
* @property {Record<string, any>} [opts] 平台扩展字段 / Platform extension fields.
|
|
20
21
|
*/
|
|
21
22
|
|
|
@@ -34,8 +35,35 @@ import { StatusTexts } from "./StatusTexts.mjs";
|
|
|
34
35
|
*/
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
|
-
*
|
|
38
|
-
* `fetch` adapter
|
|
38
|
+
* 跨平台 `fetch` 适配层。
|
|
39
|
+
* Cross-platform `fetch` adapter.
|
|
40
|
+
*
|
|
41
|
+
* 设计目标:
|
|
42
|
+
* Design goal:
|
|
43
|
+
* - 仿照 Web API `fetch`(`Window.fetch`)接口设计
|
|
44
|
+
* - Modeled after Web API `fetch` (`Window.fetch`)
|
|
45
|
+
* - 统一 VPN App、Worker 与 Node.js 环境中的请求调用
|
|
46
|
+
* - Unify request calls across VPN apps, Worker, and Node.js
|
|
47
|
+
*
|
|
48
|
+
* 功能:
|
|
49
|
+
* Features:
|
|
50
|
+
* - 统一 Quantumult X / Loon / Surge / Stash / Egern / Shadowrocket / Worker / Node.js 请求接口
|
|
51
|
+
* - Normalize request APIs across Quantumult X / Loon / Surge / Stash / Egern / Shadowrocket / Worker / Node.js
|
|
52
|
+
* - 统一返回体字段(`ok/status/statusText/body/bodyBytes`)
|
|
53
|
+
* - Normalize response fields (`ok/status/statusText/body/bodyBytes`)
|
|
54
|
+
*
|
|
55
|
+
* 与 Web `fetch` 的已知差异:
|
|
56
|
+
* Known differences from Web `fetch`:
|
|
57
|
+
* - 支持 `policy`、`auto-redirect` 等平台扩展字段
|
|
58
|
+
* - Supports platform extension fields like `policy` and `auto-redirect`
|
|
59
|
+
* - Worker / Node.js 共享基于 `fetch` 的请求分支
|
|
60
|
+
* - Worker / Node.js share the `fetch`-based request branch
|
|
61
|
+
* - `auto-cookie` 在 Worker / Node.js 共享分支中识别
|
|
62
|
+
* - `auto-cookie` is recognized by the shared Worker / Node.js branch
|
|
63
|
+
* - 非浏览器平台通过 `$httpClient/$task` 实现,不是原生 Fetch 实现
|
|
64
|
+
* - Non-browser platforms use `$httpClient/$task` instead of native Fetch engine
|
|
65
|
+
* - 返回结构包含 `statusCode/bodyBytes` 等兼容字段
|
|
66
|
+
* - Response includes compatibility fields like `statusCode/bodyBytes`
|
|
39
67
|
*
|
|
40
68
|
* @link https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch
|
|
41
69
|
* @link https://developer.mozilla.org/zh-CN/docs/Web/API/Window/fetch
|
|
@@ -45,6 +73,8 @@ import { StatusTexts } from "./StatusTexts.mjs";
|
|
|
45
73
|
* @returns {Promise<FetchResponse>}
|
|
46
74
|
*/
|
|
47
75
|
export async function fetch(resource, options = {}) {
|
|
76
|
+
// 初始化参数。
|
|
77
|
+
// Initialize request input.
|
|
48
78
|
switch (typeof resource) {
|
|
49
79
|
case "object":
|
|
50
80
|
resource = { ...options, ...resource };
|
|
@@ -56,39 +86,58 @@ export async function fetch(resource, options = {}) {
|
|
|
56
86
|
default:
|
|
57
87
|
throw new TypeError(`${Function.name}: 参数类型错误, resource 必须为对象或字符串`);
|
|
58
88
|
}
|
|
59
|
-
|
|
89
|
+
// 自动判断请求方法。
|
|
90
|
+
// Infer the HTTP method automatically.
|
|
60
91
|
if (!resource.method) {
|
|
61
92
|
resource.method = "GET";
|
|
62
93
|
if (resource.body ?? resource.bodyBytes) resource.method = "POST";
|
|
63
94
|
}
|
|
95
|
+
// 移除需要由底层实现自动生成的请求头。
|
|
96
|
+
// Remove headers that should be generated by the underlying runtime.
|
|
64
97
|
delete resource.headers?.Host;
|
|
65
98
|
delete resource.headers?.[":authority"];
|
|
66
99
|
delete resource.headers?.["Content-Length"];
|
|
67
100
|
delete resource.headers?.["content-length"];
|
|
101
|
+
// 统一请求方法为小写,方便后续索引平台 API。
|
|
102
|
+
// Normalize the method to lowercase for platform API lookups.
|
|
68
103
|
const method = resource.method.toLocaleLowerCase();
|
|
69
|
-
|
|
104
|
+
// 默认请求超时时间为 5 秒。
|
|
105
|
+
// Default request timeout to 5 seconds.
|
|
70
106
|
if (!resource.timeout) resource.timeout = 5;
|
|
71
107
|
if (resource.timeout) {
|
|
72
108
|
resource.timeout = Number.parseInt(resource.timeout, 10);
|
|
109
|
+
// 统一先转换为秒,大于 500 视为毫秒输入。
|
|
110
|
+
// Convert to seconds first and treat values above 500 as milliseconds.
|
|
73
111
|
if (resource.timeout > 500) resource.timeout = Math.round(resource.timeout / 1000);
|
|
74
112
|
}
|
|
75
113
|
if (resource.timeout) {
|
|
76
114
|
switch ($app) {
|
|
77
115
|
case "Loon":
|
|
78
116
|
case "Quantumult X":
|
|
117
|
+
case "Worker":
|
|
118
|
+
case "Node.js":
|
|
119
|
+
// 这些平台要求毫秒,因此把秒重新换算为毫秒。
|
|
120
|
+
// These platforms expect milliseconds, so convert seconds back to milliseconds.
|
|
79
121
|
resource.timeout = resource.timeout * 1000;
|
|
80
122
|
break;
|
|
123
|
+
case "Shadowrocket":
|
|
124
|
+
case "Stash":
|
|
125
|
+
case "Egern":
|
|
126
|
+
case "Surge":
|
|
81
127
|
default:
|
|
82
128
|
break;
|
|
83
129
|
}
|
|
84
130
|
}
|
|
85
|
-
|
|
131
|
+
// 根据当前平台选择请求实现。
|
|
132
|
+
// Select the request engine for the current platform.
|
|
86
133
|
switch ($app) {
|
|
87
134
|
case "Loon":
|
|
88
135
|
case "Surge":
|
|
89
136
|
case "Stash":
|
|
90
137
|
case "Egern":
|
|
91
138
|
case "Shadowrocket":
|
|
139
|
+
// 转换通用请求参数到 `$httpClient` 语义。
|
|
140
|
+
// Map shared request fields to `$httpClient` semantics.
|
|
92
141
|
if (resource.policy) {
|
|
93
142
|
switch ($app) {
|
|
94
143
|
case "Loon":
|
|
@@ -103,21 +152,28 @@ export async function fetch(resource, options = {}) {
|
|
|
103
152
|
}
|
|
104
153
|
}
|
|
105
154
|
if (typeof resource.redirection === "boolean") resource["auto-redirect"] = resource.redirection;
|
|
155
|
+
// 优先把 `bodyBytes` 映射回 `$httpClient` 能接受的 `body`。
|
|
156
|
+
// Prefer mapping `bodyBytes` back to the `body` field expected by `$httpClient`.
|
|
106
157
|
if (resource.bodyBytes && !resource.body) {
|
|
107
158
|
resource.body = resource.bodyBytes;
|
|
108
159
|
resource.bodyBytes = undefined;
|
|
109
160
|
}
|
|
161
|
+
// 根据 `Accept` 推断是否需要二进制响应体。
|
|
162
|
+
// Infer whether the response should be treated as binary from `Accept`.
|
|
110
163
|
switch ((resource.headers?.Accept || resource.headers?.accept)?.split(";")?.[0]) {
|
|
111
164
|
case "application/protobuf":
|
|
112
165
|
case "application/x-protobuf":
|
|
113
166
|
case "application/vnd.google.protobuf":
|
|
114
167
|
case "application/vnd.apple.flatbuffer":
|
|
115
168
|
case "application/grpc":
|
|
169
|
+
case "application/grpc-web":
|
|
116
170
|
case "application/grpc+proto":
|
|
117
171
|
case "application/octet-stream":
|
|
118
172
|
resource["binary-mode"] = true;
|
|
119
173
|
break;
|
|
120
174
|
}
|
|
175
|
+
// 发送 `$httpClient` 请求并归一化返回结构。
|
|
176
|
+
// Send the `$httpClient` request and normalize the response payload.
|
|
121
177
|
return new Promise((resolve, reject) => {
|
|
122
178
|
globalThis.$httpClient[method](resource, (error, response, body) => {
|
|
123
179
|
if (error) reject(error);
|
|
@@ -134,8 +190,12 @@ export async function fetch(resource, options = {}) {
|
|
|
134
190
|
});
|
|
135
191
|
});
|
|
136
192
|
case "Quantumult X":
|
|
193
|
+
// 转换 Quantumult X 专有请求参数。
|
|
194
|
+
// Map request fields to Quantumult X specific options.
|
|
137
195
|
if (resource.policy) _.set(resource, "opts.policy", resource.policy);
|
|
138
196
|
if (typeof resource["auto-redirect"] === "boolean") _.set(resource, "opts.redirection", resource["auto-redirect"]);
|
|
197
|
+
// Quantumult X 使用 `bodyBytes` 传输二进制请求体。
|
|
198
|
+
// Quantumult X uses `bodyBytes` for binary request payloads.
|
|
139
199
|
if (resource.body instanceof ArrayBuffer) {
|
|
140
200
|
resource.bodyBytes = resource.body;
|
|
141
201
|
resource.body = undefined;
|
|
@@ -143,6 +203,8 @@ export async function fetch(resource, options = {}) {
|
|
|
143
203
|
resource.bodyBytes = resource.body.buffer.slice(resource.body.byteOffset, resource.body.byteLength + resource.body.byteOffset);
|
|
144
204
|
resource.body = undefined;
|
|
145
205
|
} else if (resource.body) resource.bodyBytes = undefined;
|
|
206
|
+
// 发送请求,并用 `Promise.race` 提供统一超时保护。
|
|
207
|
+
// Send the request and enforce timeout with `Promise.race`.
|
|
146
208
|
return Promise.race([
|
|
147
209
|
globalThis.$task.fetch(resource).then(
|
|
148
210
|
response => {
|
|
@@ -155,6 +217,7 @@ export async function fetch(resource, options = {}) {
|
|
|
155
217
|
case "application/vnd.google.protobuf":
|
|
156
218
|
case "application/vnd.apple.flatbuffer":
|
|
157
219
|
case "application/grpc":
|
|
220
|
+
case "application/grpc-web":
|
|
158
221
|
case "application/grpc+proto":
|
|
159
222
|
case "application/octet-stream":
|
|
160
223
|
response.body = response.bodyBytes;
|
|
@@ -175,8 +238,67 @@ export async function fetch(resource, options = {}) {
|
|
|
175
238
|
}),
|
|
176
239
|
]);
|
|
177
240
|
case "Worker":
|
|
178
|
-
case "Node.js":
|
|
179
|
-
|
|
241
|
+
case "Node.js": {
|
|
242
|
+
// Worker 复用宿主 `fetch`;Node.js 优先复用原生 `fetch`,缺失时再回退到 `node-fetch`。
|
|
243
|
+
// Worker reuses host `fetch`; Node.js reuses native `fetch` first and falls back to `node-fetch`.
|
|
244
|
+
if (!globalThis.fetch) globalThis.fetch = require("node-fetch");
|
|
245
|
+
switch (resource["auto-cookie"]) {
|
|
246
|
+
case undefined:
|
|
247
|
+
case "true":
|
|
248
|
+
case true:
|
|
249
|
+
case "1":
|
|
250
|
+
case 1:
|
|
251
|
+
default:
|
|
252
|
+
// 仅在尚未包裹 CookieJar 时注入 `fetch-cookie`,避免重复包装。
|
|
253
|
+
// Inject `fetch-cookie` only once when a cookie jar is not already attached.
|
|
254
|
+
if (!globalThis.fetch?.cookieJar) globalThis.fetch = require("fetch-cookie").default(globalThis.fetch);
|
|
255
|
+
break;
|
|
256
|
+
case "false":
|
|
257
|
+
case false:
|
|
258
|
+
case "0":
|
|
259
|
+
case 0:
|
|
260
|
+
case "-1":
|
|
261
|
+
case -1:
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
// 将通用字段映射到 Worker / Node.js Fetch 语义。
|
|
265
|
+
// Map shared fields to Worker / Node.js Fetch semantics.
|
|
266
|
+
resource.redirect = resource.redirection ? "follow" : "manual";
|
|
267
|
+
const { url, ...options } = resource;
|
|
268
|
+
// 发起请求并归一化响应头、文本与二进制响应体。
|
|
269
|
+
// Send the request and normalize headers, text, and binary response data.
|
|
270
|
+
return Promise.race([
|
|
271
|
+
globalThis
|
|
272
|
+
.fetch(url, options)
|
|
273
|
+
.then(async response => {
|
|
274
|
+
const bodyBytes = await response.arrayBuffer();
|
|
275
|
+
let headers;
|
|
276
|
+
try {
|
|
277
|
+
headers = response.headers.raw();
|
|
278
|
+
} catch {
|
|
279
|
+
headers = Array.from(response.headers.entries()).reduce((acc, [key, value]) => {
|
|
280
|
+
acc[key] = acc[key] ? [...acc[key], value] : [value];
|
|
281
|
+
return acc;
|
|
282
|
+
}, {});
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
ok: response.ok ?? /^2\d\d$/.test(response.status),
|
|
286
|
+
status: response.status,
|
|
287
|
+
statusCode: response.status,
|
|
288
|
+
statusText: response.statusText,
|
|
289
|
+
body: new TextDecoder("utf-8").decode(bodyBytes),
|
|
290
|
+
bodyBytes: bodyBytes,
|
|
291
|
+
headers: Object.fromEntries(Object.entries(headers).map(([key, value]) => [key, key.toLowerCase() !== "set-cookie" ? value.toString() : value])),
|
|
292
|
+
};
|
|
293
|
+
})
|
|
294
|
+
.catch(error => Promise.reject(error.message)),
|
|
295
|
+
new Promise((resolve, reject) => {
|
|
296
|
+
setTimeout(() => {
|
|
297
|
+
reject(new Error(`${Function.name}: 请求超时, 请检查网络后重试`));
|
|
298
|
+
}, resource.timeout);
|
|
299
|
+
}),
|
|
300
|
+
]);
|
|
301
|
+
}
|
|
180
302
|
default:
|
|
181
303
|
throw new Error(`${Function.name}: 当前平台不支持`);
|
|
182
304
|
}
|