yeow-api 0.3.9 → 0.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/package.json +1 -1
- package/src/assets.ts +38 -14
- package/src/core.ts +4 -5
- package/src/fs.ts +204 -59
- package/src/util.ts +36 -16
package/package.json
CHANGED
package/src/assets.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { FsEncoding, ReadFileOptions } from './fs.js';
|
|
2
|
+
|
|
1
3
|
function _sendAssets(payload: Record<string, unknown>): unknown {
|
|
2
4
|
const r = $send('assets', payload);
|
|
3
5
|
if (r == null) return undefined;
|
|
@@ -15,21 +17,43 @@ function _sendAssetsAsync(payload: Record<string, unknown>): Promise<unknown> {
|
|
|
15
17
|
});
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
/** 归一化 encoding 参数(与 fs 一致:缺省 = Uint8Array;utf8/base64 → 字符串)。 */
|
|
21
|
+
function _encoding(options?: FsEncoding | ReadFileOptions): FsEncoding | undefined {
|
|
22
|
+
if (options == null) return undefined;
|
|
23
|
+
const e = typeof options === 'string' ? options : options.encoding;
|
|
24
|
+
if (e != null && e !== 'utf8' && e !== 'base64') {
|
|
25
|
+
throw new Error('Unsupported encoding: ' + String(e));
|
|
26
|
+
}
|
|
27
|
+
return e;
|
|
24
28
|
}
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
// 与 fs.readFile 相同语义:默认返回 Uint8Array;显式 utf8/base64 返回字符串。
|
|
31
|
+
type AssetsReadFn = {
|
|
32
|
+
(path: string): Promise<Uint8Array>;
|
|
33
|
+
(path: string, options: FsEncoding | (ReadFileOptions & { encoding: FsEncoding })): Promise<string>;
|
|
34
|
+
(path: string, options: ReadFileOptions): Promise<Uint8Array | string>;
|
|
35
|
+
};
|
|
36
|
+
type AssetsReadSyncFn = {
|
|
37
|
+
(path: string): Uint8Array;
|
|
38
|
+
(path: string, options: FsEncoding | (ReadFileOptions & { encoding: FsEncoding })): string;
|
|
39
|
+
(path: string, options: ReadFileOptions): Uint8Array | string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const readImpl = async (path: string, options?: FsEncoding | ReadFileOptions): Promise<Uint8Array | string> => {
|
|
43
|
+
const enc = _encoding(options);
|
|
44
|
+
const r = await _sendAssetsAsync({ t: enc === 'utf8' ? 'read' : 'readBase64', p: { path } }) as { data: string };
|
|
45
|
+
if (enc === 'utf8' || enc === 'base64') return r.data;
|
|
46
|
+
return Uint8Array.fromBase64(r.data);
|
|
47
|
+
};
|
|
48
|
+
const readSyncImpl = (path: string, options?: FsEncoding | ReadFileOptions): Uint8Array | string => {
|
|
49
|
+
const enc = _encoding(options);
|
|
50
|
+
const r = _sendAssets({ t: enc === 'utf8' ? 'read' : 'readBase64', p: { path } }) as { data: string };
|
|
51
|
+
if (enc === 'utf8' || enc === 'base64') return r.data;
|
|
52
|
+
return Uint8Array.fromBase64(r.data);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const read = readImpl as AssetsReadFn;
|
|
56
|
+
export const readSync = readSyncImpl as AssetsReadSyncFn;
|
|
33
57
|
|
|
34
58
|
export async function extract(path: string, dest?: string): Promise<string> {
|
|
35
59
|
const p: Record<string, unknown> = { path };
|
|
@@ -55,4 +79,4 @@ export function extractDirSync(path: string, dest?: string): string {
|
|
|
55
79
|
return (_sendAssets({ t: 'extractDir', p }) as { path: string }).path;
|
|
56
80
|
}
|
|
57
81
|
|
|
58
|
-
export const assets = { read, readSync,
|
|
82
|
+
export const assets = { read, readSync, extract, extractSync, extractDir, extractDirSync };
|
package/src/core.ts
CHANGED
|
@@ -43,16 +43,15 @@ export {
|
|
|
43
43
|
export type { TpsInfo } from './server.js';
|
|
44
44
|
export {
|
|
45
45
|
fs,
|
|
46
|
-
readFile, readFileSync,
|
|
47
|
-
writeFile, writeFileSync,
|
|
46
|
+
readFile, readFileSync,
|
|
47
|
+
writeFile, writeFileSync,
|
|
48
48
|
appendFile, appendFileSync,
|
|
49
49
|
exists, existsSync, stat, statSync,
|
|
50
50
|
isDirectory, isDirectorySync,
|
|
51
51
|
deleteFile, deleteFileSync, mkdir, mkdirSync, list, listSync,
|
|
52
52
|
} from './fs.js';
|
|
53
|
-
export type { FileStat } from './fs.js';
|
|
53
|
+
export type { FileStat, FsEncoding, FsData, ReadFileOptions, WriteFileOptions } from './fs.js';
|
|
54
54
|
export { assets, read as assetsRead, readSync as assetsReadSync,
|
|
55
|
-
readBase64 as assetsReadBase64, readBase64Sync as assetsReadBase64Sync,
|
|
56
55
|
extract as assetsExtract, extractSync as assetsExtractSync,
|
|
57
56
|
extractDir as assetsExtractDir, extractDirSync as assetsExtractDirSync } from './assets.js';
|
|
58
57
|
export { path } from './path.js';
|
|
@@ -112,4 +111,4 @@ export {
|
|
|
112
111
|
bytesToString, bytesToStringAsync,
|
|
113
112
|
Gzip,
|
|
114
113
|
} from './util.js';
|
|
115
|
-
export type { GzipCompressor, GzipDecompressor } from './util.js';
|
|
114
|
+
export type { GzipCompressor, GzipDecompressor, GzipCompressOptions, GzipDecompressOptions } from './util.js';
|
package/src/fs.ts
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
type FsLevel = 'plugin' | 'server' | 'outer';
|
|
2
2
|
|
|
3
|
-
import { stringToBytes } from './util.js';
|
|
3
|
+
import { stringToBytes, bytesToString } from './util.js';
|
|
4
|
+
|
|
5
|
+
/** 文件编码:utf8 = 文本;base64 = 将字符串视为 Base64 编码的二进制数据。 */
|
|
6
|
+
export type FsEncoding = 'utf8' | 'base64';
|
|
7
|
+
|
|
8
|
+
/** readFile 选项(Node 风格:缺省 encoding = 返回 Uint8Array)。 */
|
|
9
|
+
export interface ReadFileOptions {
|
|
10
|
+
encoding?: FsEncoding;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** writeFile / appendFile 选项:字符串默认 utf8;可指定 base64 将字符串视为二进制。 */
|
|
14
|
+
export interface WriteFileOptions {
|
|
15
|
+
encoding?: FsEncoding;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type FsData = string | Uint8Array;
|
|
4
19
|
|
|
5
20
|
function _sendFs(payload: Record<string, unknown>): unknown {
|
|
6
21
|
const r = $send('fs', payload);
|
|
@@ -19,46 +34,138 @@ function _sendFsAsync(payload: Record<string, unknown>): Promise<unknown> {
|
|
|
19
34
|
});
|
|
20
35
|
}
|
|
21
36
|
|
|
37
|
+
/** 归一化 encoding 参数(string | { encoding });未知编码抛错。 */
|
|
38
|
+
function _encoding(options?: FsEncoding | { encoding?: FsEncoding }): FsEncoding | undefined {
|
|
39
|
+
if (options == null) return undefined;
|
|
40
|
+
const e = typeof options === 'string' ? options : options.encoding;
|
|
41
|
+
if (e != null && e !== 'utf8' && e !== 'base64') {
|
|
42
|
+
throw new Error('Unsupported encoding: ' + String(e));
|
|
43
|
+
}
|
|
44
|
+
return e;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** UTF-8 流式解码器:跨块保留不完整的多字节序列,EOF 时冲刷(非法序列替换为 U+FFFD)。 */
|
|
48
|
+
function _makeUtf8StreamDecoder() {
|
|
49
|
+
let pending: Uint8Array = new Uint8Array(0);
|
|
50
|
+
|
|
51
|
+
const seqLen = (b: number) => {
|
|
52
|
+
if (b < 0x80) return 1;
|
|
53
|
+
if ((b & 0xE0) === 0xC0) return 2;
|
|
54
|
+
if ((b & 0xF0) === 0xE0) return 3;
|
|
55
|
+
if ((b & 0xF8) === 0xF0) return 4;
|
|
56
|
+
return 1; // 非法起始字节:交给 bytesToString 以替换字符处理
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
push(chunk: Uint8Array): string | null {
|
|
61
|
+
const buf = pending.length
|
|
62
|
+
? (() => { const b = new Uint8Array(pending.length + chunk.length); b.set(pending, 0); b.set(chunk, pending.length); return b; })()
|
|
63
|
+
: chunk;
|
|
64
|
+
if (!buf.length) return null;
|
|
65
|
+
|
|
66
|
+
let cut = buf.length;
|
|
67
|
+
const last = buf.length - 1;
|
|
68
|
+
if ((buf[last] & 0x80) !== 0) {
|
|
69
|
+
// 回溯找到可能的多字节序列起点(最多 3 个续字节)
|
|
70
|
+
let start = last;
|
|
71
|
+
let cont = 0;
|
|
72
|
+
while (cont < 3 && start > 0 && (buf[start] & 0xC0) === 0x80) { start--; cont++; }
|
|
73
|
+
const need = seqLen(buf[start]);
|
|
74
|
+
const have = buf.length - start;
|
|
75
|
+
if (need > 1 && have < need && need <= 4) {
|
|
76
|
+
// 仅当确实是合法起始字节时视为“序列未结束”,留给下一块
|
|
77
|
+
const validLead =
|
|
78
|
+
(need === 2 && (buf[start] & 0xE0) === 0xC0) ||
|
|
79
|
+
(need === 3 && (buf[start] & 0xF0) === 0xE0) ||
|
|
80
|
+
(need === 4 && (buf[start] & 0xF8) === 0xF0);
|
|
81
|
+
if (validLead) cut = start;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
pending = cut === buf.length ? new Uint8Array(0) : buf.slice(cut);
|
|
86
|
+
const complete = cut === buf.length ? buf : buf.subarray(0, cut);
|
|
87
|
+
return complete.length ? bytesToString(complete) : null;
|
|
88
|
+
},
|
|
89
|
+
/** EOF:把剩余的不完整序列交给 Java 解码(通常产出 U+FFFD)。 */
|
|
90
|
+
flush(): string | null {
|
|
91
|
+
if (!pending.length) return null;
|
|
92
|
+
const s = bytesToString(pending);
|
|
93
|
+
pending = new Uint8Array(0);
|
|
94
|
+
return s;
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
22
99
|
/** 按 fs 级别(plugin/server/outer)生成全套文件操作。 */
|
|
23
100
|
function _makeFs(level: FsLevel) {
|
|
24
101
|
const t = (op: string) => `${level}.${op}`;
|
|
25
102
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
function readFileBase64Sync(path: string): string {
|
|
39
|
-
return (_sendFs({ t: t('readBase64'), p: { path } }) as { data: string }).data;
|
|
40
|
-
}
|
|
103
|
+
// ── 读文件:默认 Uint8Array;encoding 指定后返回字符串 ──────────
|
|
104
|
+
type ReadFileFn = {
|
|
105
|
+
(path: string): Promise<Uint8Array>;
|
|
106
|
+
(path: string, options: FsEncoding | (ReadFileOptions & { encoding: FsEncoding })): Promise<string>;
|
|
107
|
+
(path: string, options: ReadFileOptions): Promise<Uint8Array | string>;
|
|
108
|
+
};
|
|
109
|
+
type ReadFileSyncFn = {
|
|
110
|
+
(path: string): Uint8Array;
|
|
111
|
+
(path: string, options: FsEncoding | (ReadFileOptions & { encoding: FsEncoding })): string;
|
|
112
|
+
(path: string, options: ReadFileOptions): Uint8Array | string;
|
|
113
|
+
};
|
|
41
114
|
|
|
42
|
-
async
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
115
|
+
const readFileImpl = async (path: string, options?: FsEncoding | ReadFileOptions): Promise<Uint8Array | string> => {
|
|
116
|
+
const enc = _encoding(options);
|
|
117
|
+
const r = await _sendFsAsync({ t: t(enc === 'utf8' ? 'readFile' : 'readBase64'), p: { path } }) as { data: string };
|
|
118
|
+
if (enc === 'utf8' || enc === 'base64') return r.data;
|
|
119
|
+
return Uint8Array.fromBase64(r.data);
|
|
120
|
+
};
|
|
121
|
+
const readFileSyncImpl = (path: string, options?: FsEncoding | ReadFileOptions): Uint8Array | string => {
|
|
122
|
+
const enc = _encoding(options);
|
|
123
|
+
const r = _sendFs({ t: t(enc === 'utf8' ? 'readFile' : 'readBase64'), p: { path } }) as { data: string };
|
|
124
|
+
if (enc === 'utf8' || enc === 'base64') return r.data;
|
|
125
|
+
return Uint8Array.fromBase64(r.data);
|
|
126
|
+
};
|
|
127
|
+
const readFile = readFileImpl as ReadFileFn;
|
|
128
|
+
const readFileSync = readFileSyncImpl as ReadFileSyncFn;
|
|
48
129
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
130
|
+
// ── 写文件:string 默认 utf8(可指定 base64),Uint8Array 写原始字节 ──
|
|
131
|
+
const writeFile = async (path: string, data: FsData, options?: FsEncoding | WriteFileOptions): Promise<void> => {
|
|
132
|
+
const enc = _encoding(options);
|
|
133
|
+
if (typeof data === 'string') {
|
|
134
|
+
if (enc === 'base64') await _sendFsAsync({ t: t('writeBase64'), p: { path, data } });
|
|
135
|
+
else await _sendFsAsync({ t: t('writeFile'), p: { path, data } });
|
|
136
|
+
} else {
|
|
137
|
+
await _sendFsAsync({ t: t('writeBase64'), p: { path, data: data.toBase64() } });
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const writeFileSync = (path: string, data: FsData, options?: FsEncoding | WriteFileOptions): void => {
|
|
141
|
+
const enc = _encoding(options);
|
|
142
|
+
if (typeof data === 'string') {
|
|
143
|
+
if (enc === 'base64') _sendFs({ t: t('writeBase64'), p: { path, data } });
|
|
144
|
+
else _sendFs({ t: t('writeFile'), p: { path, data } });
|
|
145
|
+
} else {
|
|
146
|
+
_sendFs({ t: t('writeBase64'), p: { path, data: data.toBase64() } });
|
|
147
|
+
}
|
|
148
|
+
};
|
|
55
149
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
150
|
+
// ── 追加:与 writeFile 相同的数据/编码语义 ────────────────────────
|
|
151
|
+
const appendFile = async (path: string, data: FsData, options?: FsEncoding | WriteFileOptions): Promise<void> => {
|
|
152
|
+
const enc = _encoding(options);
|
|
153
|
+
if (typeof data === 'string') {
|
|
154
|
+
if (enc === 'base64') await _sendFsAsync({ t: t('appendBase64'), p: { path, data } });
|
|
155
|
+
else await _sendFsAsync({ t: t('appendFile'), p: { path, data } });
|
|
156
|
+
} else {
|
|
157
|
+
await _sendFsAsync({ t: t('appendBase64'), p: { path, data: data.toBase64() } });
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
const appendFileSync = (path: string, data: FsData, options?: FsEncoding | WriteFileOptions): void => {
|
|
161
|
+
const enc = _encoding(options);
|
|
162
|
+
if (typeof data === 'string') {
|
|
163
|
+
if (enc === 'base64') _sendFs({ t: t('appendBase64'), p: { path, data } });
|
|
164
|
+
else _sendFs({ t: t('appendFile'), p: { path, data } });
|
|
165
|
+
} else {
|
|
166
|
+
_sendFs({ t: t('appendBase64'), p: { path, data: data.toBase64() } });
|
|
167
|
+
}
|
|
168
|
+
};
|
|
62
169
|
|
|
63
170
|
async function exists(path: string): Promise<boolean> {
|
|
64
171
|
const r = await _sendFsAsync({ t: t('exists'), p: { path } });
|
|
@@ -123,19 +230,47 @@ function _makeFs(level: FsLevel) {
|
|
|
123
230
|
return (_sendFs({ t: t('getServerPath') }) as { path: string }).path;
|
|
124
231
|
}
|
|
125
232
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
233
|
+
// ── 读流:encoding 在创建时固定(默认 Uint8Array;utf8/base64 → string)──
|
|
234
|
+
type CreateReadStreamFn = {
|
|
235
|
+
(path: string): Promise<ReadStream<Uint8Array>>;
|
|
236
|
+
(path: string, options: ReadStreamOptions & { encoding?: undefined }): Promise<ReadStream<Uint8Array>>;
|
|
237
|
+
(path: string, options: ReadStreamOptions & { encoding: FsEncoding }): Promise<ReadStream<string>>;
|
|
238
|
+
(path: string, options: ReadStreamOptions): Promise<ReadStream<Uint8Array> | ReadStream<string>>;
|
|
239
|
+
};
|
|
130
240
|
|
|
241
|
+
const createReadStreamImpl = async (path: string, options?: ReadStreamOptions): Promise<ReadStream<Uint8Array> | ReadStream<string>> => {
|
|
242
|
+
const enc = _encoding(options);
|
|
243
|
+
const r = await _sendFsAsync({ t: t('openRead'), p: { path, start: options?.start, end: options?.end } }) as { id: string };
|
|
244
|
+
if (enc === 'base64') {
|
|
245
|
+
return _makeReadStream((op, p2) => _sendFsAsync({ t: t(op), p: p2 }), r.id, {
|
|
246
|
+
push: (b64: string) => b64,
|
|
247
|
+
flush: () => null,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
if (enc === 'utf8') {
|
|
251
|
+
const dec = _makeUtf8StreamDecoder();
|
|
252
|
+
return _makeReadStream((op, p2) => _sendFsAsync({ t: t(op), p: p2 }), r.id, {
|
|
253
|
+
push: (b64: string) => dec.push(Uint8Array.fromBase64(b64)),
|
|
254
|
+
flush: () => dec.flush(),
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
return _makeReadStream((op, p2) => _sendFsAsync({ t: t(op), p: p2 }), r.id, {
|
|
258
|
+
push: (b64: string) => Uint8Array.fromBase64(b64),
|
|
259
|
+
flush: () => null,
|
|
260
|
+
});
|
|
261
|
+
};
|
|
262
|
+
const createReadStream = createReadStreamImpl as CreateReadStreamFn;
|
|
263
|
+
|
|
264
|
+
// ── 写流:与 writeFile 相同的数据/编码语义(encoding 创建时固定)──
|
|
131
265
|
async function createWriteStream(path: string, options?: WriteStreamOptions): Promise<WriteStream> {
|
|
132
|
-
const
|
|
133
|
-
|
|
266
|
+
const enc = _encoding(options);
|
|
267
|
+
const r = await _sendFsAsync({ t: t('openWrite'), p: { path, flags: options?.flags } }) as { id: string };
|
|
268
|
+
return _makeWriteStream((op, p2) => _sendFsAsync({ t: t(op), p: p2 }), r.id, enc);
|
|
134
269
|
}
|
|
135
270
|
|
|
136
271
|
return {
|
|
137
|
-
readFile, readFileSync,
|
|
138
|
-
writeFile, writeFileSync,
|
|
272
|
+
readFile, readFileSync,
|
|
273
|
+
writeFile, writeFileSync,
|
|
139
274
|
appendFile, appendFileSync,
|
|
140
275
|
exists, existsSync, stat, statSync,
|
|
141
276
|
isDirectory, isDirectorySync,
|
|
@@ -157,12 +292,8 @@ export const fs = {
|
|
|
157
292
|
// 顶层函数 = plugin 级别(与 fs.* 一致)
|
|
158
293
|
export const readFile = fs.readFile;
|
|
159
294
|
export const readFileSync = fs.readFileSync;
|
|
160
|
-
export const readFileBase64 = fs.readFileBase64;
|
|
161
|
-
export const readFileBase64Sync = fs.readFileBase64Sync;
|
|
162
295
|
export const writeFile = fs.writeFile;
|
|
163
296
|
export const writeFileSync = fs.writeFileSync;
|
|
164
|
-
export const writeFileBase64 = fs.writeFileBase64;
|
|
165
|
-
export const writeFileBase64Sync = fs.writeFileBase64Sync;
|
|
166
297
|
export const appendFile = fs.appendFile;
|
|
167
298
|
export const appendFileSync = fs.appendFileSync;
|
|
168
299
|
export const exists = fs.exists;
|
|
@@ -182,15 +313,17 @@ export const createWriteStream = fs.createWriteStream;
|
|
|
182
313
|
|
|
183
314
|
// ── 流式读写(有状态句柄;背压 = 显式响应——每个操作 await 结果后才发起下一块)──
|
|
184
315
|
|
|
185
|
-
/** 读流选项:字节偏移区间(start 含、end
|
|
316
|
+
/** 读流选项:字节偏移区间(start 含、end 含);encoding 创建时固定(默认 Uint8Array)。 */
|
|
186
317
|
export interface ReadStreamOptions {
|
|
187
318
|
start?: number;
|
|
188
319
|
end?: number;
|
|
320
|
+
encoding?: FsEncoding;
|
|
189
321
|
}
|
|
190
322
|
|
|
191
|
-
/**
|
|
323
|
+
/** 写流选项:打开模式 + 编码(encoding 创建时固定,运行时不可修改)。 */
|
|
192
324
|
export interface WriteStreamOptions {
|
|
193
325
|
flags?: 'w' | 'a' | 'wx';
|
|
326
|
+
encoding?: FsEncoding;
|
|
194
327
|
}
|
|
195
328
|
|
|
196
329
|
/** 文件状态(stat)。mtimeMs / ctimeMs 为 epoch 毫秒。 */
|
|
@@ -203,32 +336,39 @@ export interface FileStat {
|
|
|
203
336
|
}
|
|
204
337
|
|
|
205
338
|
/** 文件读流:read() 一次返回一块(默认 1 MiB),null = EOF;可 for await。 */
|
|
206
|
-
export interface ReadStream {
|
|
207
|
-
read(maxBytes?: number): Promise<
|
|
339
|
+
export interface ReadStream<Chunk = Uint8Array> {
|
|
340
|
+
read(maxBytes?: number): Promise<Chunk | null>;
|
|
208
341
|
close(): Promise<void>;
|
|
209
|
-
[Symbol.asyncIterator](): AsyncIterator<
|
|
342
|
+
[Symbol.asyncIterator](): AsyncIterator<Chunk>;
|
|
210
343
|
}
|
|
211
344
|
|
|
212
345
|
/** 文件写流:write(chunk) 等到写入完成(显式响应背压),end() 冲刷并关闭。 */
|
|
213
346
|
export interface WriteStream {
|
|
347
|
+
/** 创建时固定的编码;未指定时字符串 chunk 按 UTF-8 编码。运行时不可修改。 */
|
|
348
|
+
readonly encoding?: FsEncoding;
|
|
214
349
|
write(chunk: Uint8Array | string): Promise<void>;
|
|
215
350
|
/** 冲刷缓冲并关闭(调用后不可再 write)。 */
|
|
216
351
|
end(): Promise<void>;
|
|
217
352
|
close(): Promise<void>;
|
|
218
353
|
}
|
|
219
354
|
|
|
220
|
-
function _makeReadStream(
|
|
355
|
+
function _makeReadStream<Chunk>(
|
|
221
356
|
op: (name: string, p: Record<string, unknown>) => Promise<unknown>,
|
|
222
357
|
id: string,
|
|
223
|
-
):
|
|
358
|
+
decoder: { push(b64: string): Chunk | null; flush(): Chunk | null },
|
|
359
|
+
): ReadStream<Chunk> {
|
|
224
360
|
let closed = false;
|
|
225
361
|
const check = () => { if (closed) throw new Error('read stream closed'); };
|
|
226
362
|
return {
|
|
227
|
-
async read(maxBytes?: number) {
|
|
363
|
+
async read(maxBytes?: number): Promise<Chunk | null> {
|
|
228
364
|
check();
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
365
|
+
while (true) {
|
|
366
|
+
const r = await op('read', { id, maxBytes }) as { data?: string; eof?: boolean };
|
|
367
|
+
if (r.eof) return decoder.flush();
|
|
368
|
+
const chunk = decoder.push(r.data as string);
|
|
369
|
+
// UTF-8 解码器可能因跨块多字节序列暂未产出——继续读下一块
|
|
370
|
+
if (chunk !== null) return chunk;
|
|
371
|
+
}
|
|
232
372
|
},
|
|
233
373
|
async close() {
|
|
234
374
|
if (closed) return;
|
|
@@ -248,14 +388,19 @@ function _makeReadStream(
|
|
|
248
388
|
function _makeWriteStream(
|
|
249
389
|
op: (name: string, p: Record<string, unknown>) => Promise<unknown>,
|
|
250
390
|
id: string,
|
|
391
|
+
encoding?: FsEncoding,
|
|
251
392
|
): WriteStream {
|
|
252
393
|
let closed = false;
|
|
253
394
|
const check = () => { if (closed) throw new Error('write stream closed'); };
|
|
395
|
+
const toBytes = (chunk: Uint8Array | string): Uint8Array => {
|
|
396
|
+
if (typeof chunk !== 'string') return chunk; // Uint8Array 始终按原始字节写入
|
|
397
|
+
return encoding === 'base64' ? Uint8Array.fromBase64(chunk) : stringToBytes(chunk);
|
|
398
|
+
};
|
|
254
399
|
return {
|
|
400
|
+
encoding,
|
|
255
401
|
async write(chunk: Uint8Array | string) {
|
|
256
402
|
check();
|
|
257
|
-
|
|
258
|
-
await op('write', { id, data: data.toBase64() });
|
|
403
|
+
await op('write', { id, data: toBytes(chunk).toBase64() });
|
|
259
404
|
},
|
|
260
405
|
async end() {
|
|
261
406
|
check();
|
package/src/util.ts
CHANGED
|
@@ -73,28 +73,48 @@ export interface GzipDecompressor {
|
|
|
73
73
|
close(): Promise<void>;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/** 压缩选项:level 0-9(默认引擎默认级别);raw = 原始 deflate(无 GZIP 头/尾/CRC)。 */
|
|
77
|
+
export interface GzipCompressOptions {
|
|
78
|
+
level?: number;
|
|
79
|
+
raw?: boolean;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 解压选项:raw = 原始 deflate(无 GZIP 头/尾/CRC 校验)。 */
|
|
83
|
+
export interface GzipDecompressOptions {
|
|
84
|
+
raw?: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 兼容旧式数字 level 参数:Gzip.compress(data, 6) 仍可用。 */
|
|
88
|
+
type LevelArg = number | GzipCompressOptions | undefined;
|
|
89
|
+
|
|
90
|
+
function normalizeOptions(o?: LevelArg): GzipCompressOptions {
|
|
91
|
+
return typeof o === 'number' ? { level: o } : (o ?? {});
|
|
92
|
+
}
|
|
93
|
+
|
|
76
94
|
export const Gzip = {
|
|
77
|
-
/**
|
|
78
|
-
compress(data: Uint8Array | string,
|
|
79
|
-
|
|
95
|
+
/** 一次性压缩(level 0-9;raw=true 输出原始 deflate 流)。输入 string 视为 UTF-8 文本。 */
|
|
96
|
+
compress(data: Uint8Array | string, options?: LevelArg): Promise<Uint8Array> {
|
|
97
|
+
const o = normalizeOptions(options);
|
|
98
|
+
return sendAsync<{ data: string }>('gzip.compress', { data: toB64(data), level: o.level, raw: o.raw })
|
|
80
99
|
.then((r) => Uint8Array.fromBase64(r.data));
|
|
81
100
|
},
|
|
82
|
-
/**
|
|
83
|
-
compressSync(data: Uint8Array | string,
|
|
84
|
-
|
|
101
|
+
/** 一次性压缩(同步版)。 */
|
|
102
|
+
compressSync(data: Uint8Array | string, options?: LevelArg): Uint8Array {
|
|
103
|
+
const o = normalizeOptions(options);
|
|
104
|
+
return Uint8Array.fromBase64(send<{ data: string }>('gzip.compress', { data: toB64(data), level: o.level, raw: o.raw }).data);
|
|
85
105
|
},
|
|
86
|
-
/**
|
|
87
|
-
decompress(data: Uint8Array | string): Promise<Uint8Array> {
|
|
88
|
-
return sendAsync<{ data: string }>('gzip.decompress', { data: toB64(data) })
|
|
106
|
+
/** 一次性解压(输出上限 256 MiB,超限报错——防压缩炸弹;上限可在 config.yml util 段调整)。 */
|
|
107
|
+
decompress(data: Uint8Array | string, options?: GzipDecompressOptions): Promise<Uint8Array> {
|
|
108
|
+
return sendAsync<{ data: string }>('gzip.decompress', { data: toB64(data), raw: options?.raw })
|
|
89
109
|
.then((r) => Uint8Array.fromBase64(r.data));
|
|
90
110
|
},
|
|
91
|
-
/**
|
|
92
|
-
decompressSync(data: Uint8Array | string): Uint8Array {
|
|
93
|
-
return Uint8Array.fromBase64(send<{ data: string }>('gzip.decompress', { data: toB64(data) }).data);
|
|
111
|
+
/** 一次性解压(同步版)。 */
|
|
112
|
+
decompressSync(data: Uint8Array | string, options?: GzipDecompressOptions): Uint8Array {
|
|
113
|
+
return Uint8Array.fromBase64(send<{ data: string }>('gzip.decompress', { data: toB64(data), raw: options?.raw }).data);
|
|
94
114
|
},
|
|
95
115
|
/** 创建分块压缩器(流式管道):create → write×n → finish → close。 */
|
|
96
|
-
async createCompressor(options?:
|
|
97
|
-
const r = await sendAsync<{ id: string }>('gzip.compressor.create', { level: options?.level });
|
|
116
|
+
async createCompressor(options?: GzipCompressOptions): Promise<GzipCompressor> {
|
|
117
|
+
const r = await sendAsync<{ id: string }>('gzip.compressor.create', { level: options?.level, raw: options?.raw });
|
|
98
118
|
let closed = false;
|
|
99
119
|
const check = () => { if (closed) throw new Error('compressor closed'); };
|
|
100
120
|
return {
|
|
@@ -116,8 +136,8 @@ export const Gzip = {
|
|
|
116
136
|
};
|
|
117
137
|
},
|
|
118
138
|
/** 创建分块解压器(流式管道):create → write×n → finish → close。 */
|
|
119
|
-
async createDecompressor(): Promise<GzipDecompressor> {
|
|
120
|
-
const r = await sendAsync<{ id: string }>('gzip.decompressor.create', {});
|
|
139
|
+
async createDecompressor(options?: GzipDecompressOptions): Promise<GzipDecompressor> {
|
|
140
|
+
const r = await sendAsync<{ id: string }>('gzip.decompressor.create', { raw: options?.raw });
|
|
121
141
|
let closed = false;
|
|
122
142
|
const check = () => { if (closed) throw new Error('decompressor closed'); };
|
|
123
143
|
return {
|