yeow-api 0.3.5 → 0.3.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yeow-api",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "description": "Yeow API",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
package/src/core.ts CHANGED
@@ -54,6 +54,8 @@ export { assets, read as assetsRead, readSync as assetsReadSync,
54
54
  extract as assetsExtract, extractSync as assetsExtractSync,
55
55
  extractDir as assetsExtractDir, extractDirSync as assetsExtractDirSync } from './assets.js';
56
56
  export { path } from './path.js';
57
+ export { createReadStream, createWriteStream } from './fs.js';
58
+ export type { ReadStream, WriteStream } from './fs.js';
57
59
  export { listen, respond, close, request, requestSync } from './http.js';
58
60
  export type { RespondOptions } from './http.js';
59
61
  export { logError } from './log-error.js';
@@ -106,6 +108,6 @@ export type { WorkerOptions } from './worker.js';
106
108
  export {
107
109
  stringToBytes, stringToBytesAsync,
108
110
  bytesToString, bytesToStringAsync,
109
- gzipCompress, gzipCompressSync,
110
- gzipDecompress, gzipDecompressSync,
111
+ Gzip,
111
112
  } from './util.js';
113
+ export type { GzipCompressor, GzipDecompressor } from './util.js';
package/src/fs.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  type FsLevel = 'plugin' | 'server' | 'outer';
2
2
 
3
+ import { stringToBytes } from './util.js';
4
+
3
5
  function _sendFs(payload: Record<string, unknown>): unknown {
4
6
  const r = $send('fs', payload);
5
7
  if (r == null) return undefined;
@@ -114,12 +116,23 @@ function _makeFs(level: FsLevel) {
114
116
  return (_sendFs({ t: t('getServerPath') }) as { path: string }).path;
115
117
  }
116
118
 
119
+ async function createReadStream(path: string): Promise<ReadStream> {
120
+ const r = await _sendFsAsync({ t: t('openRead'), p: { path } }) as { id: string };
121
+ return _makeReadStream((op, p2) => _sendFsAsync({ t: t(op), p: p2 }), r.id);
122
+ }
123
+
124
+ async function createWriteStream(path: string): Promise<WriteStream> {
125
+ const r = await _sendFsAsync({ t: t('openWrite'), p: { path } }) as { id: string };
126
+ return _makeWriteStream((op, p2) => _sendFsAsync({ t: t(op), p: p2 }), r.id);
127
+ }
128
+
117
129
  return {
118
130
  readFile, readFileSync, readFileBase64, readFileBase64Sync,
119
131
  writeFile, writeFileSync, writeFileBase64, writeFileBase64Sync,
120
132
  appendFile, appendFileSync,
121
133
  exists, existsSync, isDirectory, isDirectorySync,
122
134
  deleteFile, deleteFileSync, mkdir, mkdirSync, list, listSync,
135
+ createReadStream, createWriteStream,
123
136
  // outer 专属能力(systemPaths / getServerPath)
124
137
  ...(level === 'outer' ? { systemPaths, systemPathsSync, getServerPath, getServerPathSync } : {}),
125
138
  };
@@ -154,3 +167,75 @@ export const mkdir = fs.mkdir;
154
167
  export const mkdirSync = fs.mkdirSync;
155
168
  export const list = fs.list;
156
169
  export const listSync = fs.listSync;
170
+ export const createReadStream = fs.createReadStream;
171
+ export const createWriteStream = fs.createWriteStream;
172
+
173
+ // ── 流式读写(有状态句柄;背压 = 显式响应——每个操作 await 结果后才发起下一块)──
174
+
175
+ /** 文件读流:read() 一次返回一块(默认 1 MiB),null = EOF;可 for await。 */
176
+ export interface ReadStream {
177
+ read(maxBytes?: number): Promise<Uint8Array | null>;
178
+ close(): Promise<void>;
179
+ [Symbol.asyncIterator](): AsyncIterator<Uint8Array>;
180
+ }
181
+
182
+ /** 文件写流:write(chunk) 等到写入完成(显式响应背压),end() 冲刷并关闭。 */
183
+ export interface WriteStream {
184
+ write(chunk: Uint8Array | string): Promise<void>;
185
+ /** 冲刷缓冲并关闭(调用后不可再 write)。 */
186
+ end(): Promise<void>;
187
+ close(): Promise<void>;
188
+ }
189
+
190
+ function _makeReadStream(
191
+ op: (name: string, p: Record<string, unknown>) => Promise<unknown>,
192
+ id: string,
193
+ ): ReadStream {
194
+ let closed = false;
195
+ const check = () => { if (closed) throw new Error('read stream closed'); };
196
+ return {
197
+ async read(maxBytes?: number) {
198
+ check();
199
+ const r = await op('read', { id, maxBytes }) as { data?: string; eof?: boolean };
200
+ if (r.eof) return null;
201
+ return Uint8Array.fromBase64(r.data as string);
202
+ },
203
+ async close() {
204
+ if (closed) return;
205
+ closed = true;
206
+ await op('close', { id });
207
+ },
208
+ async *[Symbol.asyncIterator]() {
209
+ while (true) {
210
+ const chunk = await this.read();
211
+ if (chunk === null) break;
212
+ yield chunk;
213
+ }
214
+ },
215
+ };
216
+ }
217
+
218
+ function _makeWriteStream(
219
+ op: (name: string, p: Record<string, unknown>) => Promise<unknown>,
220
+ id: string,
221
+ ): WriteStream {
222
+ let closed = false;
223
+ const check = () => { if (closed) throw new Error('write stream closed'); };
224
+ return {
225
+ async write(chunk: Uint8Array | string) {
226
+ check();
227
+ const data = typeof chunk === 'string' ? stringToBytes(chunk) : chunk;
228
+ await op('write', { id, data: data.toBase64() });
229
+ },
230
+ async end() {
231
+ check();
232
+ closed = true;
233
+ await op('end', { id });
234
+ },
235
+ async close() {
236
+ if (closed) return;
237
+ closed = true;
238
+ await op('close', { id });
239
+ },
240
+ };
241
+ }
package/src/path.ts CHANGED
@@ -3,15 +3,20 @@ export function join(...segments: (string | null | undefined)[]): string {
3
3
  }
4
4
 
5
5
  export function basename(p: string): string {
6
- const s = p.replace(/\/+$/, '');
7
- const i = s.lastIndexOf('/');
6
+ // 双分隔符:兼容 Windows 反斜杠路径(fs 通道在 Windows 上返回 `\` 路径)
7
+ const s = p.replace(/[\\/]+$/, '');
8
+ const i = Math.max(s.lastIndexOf('/'), s.lastIndexOf('\\'));
8
9
  return i === -1 ? s : s.substring(i + 1);
9
10
  }
10
11
 
11
12
  export function dirname(p: string): string {
12
- const s = p.replace(/\/+$/, '');
13
- const i = s.lastIndexOf('/');
14
- return i === -1 ? '.' : s.substring(0, i) || '/';
13
+ const s = p.replace(/[\\/]+$/, '');
14
+ const i = Math.max(s.lastIndexOf('/'), s.lastIndexOf('\\'));
15
+ if (i === -1) return '.';
16
+ const d = s.substring(0, i);
17
+ // 根:POSIX `/`;Windows 盘符根(`C:\` → `C:\`,`C:\a` → `C:` 兼容)
18
+ if (d === '') return s.length >= 3 && s[1] === ':' ? s.substring(0, 3) : '/';
19
+ return d;
15
20
  }
16
21
 
17
22
  export function extname(p: string): string {
package/src/util.ts CHANGED
@@ -1,8 +1,9 @@
1
- // util 通道:gzip 压缩/解压 + UTF-8 ↔ 字节转换。
1
+ // util 通道:gzip 压缩/解压(一次性 + 流式分块)+ UTF-8 ↔ 字节转换。
2
2
  //
3
3
  // 协议层字节数据以 base64 字符串承载(引擎原生 Uint8Array.toBase64()/fromBase64()
4
4
  // 负责转换)——本模块输入输出一律 Uint8Array / string,**不暴露 base64**。
5
- // encode/decode 的语义是 buffer ↔ 字符串;无流式接口(一次性整体处理)。
5
+ // 流式 API:背压基于**显式响应**——每个操作 await 结果后才发起下一块;
6
+ // 块大小由调用方决定(建议 ≥256 KiB,摊销跨线程往返开销)。
6
7
 
7
8
  /** util 通道同步调用:err → 抛 Error。 */
8
9
  function send<T>(t: string, p: Record<string, unknown>): T {
@@ -52,26 +53,90 @@ export function bytesToStringAsync(bytes: Uint8Array): Promise<string> {
52
53
  return sendAsync<{ data: string }>('decode.utf8', { data: bytes.toBase64() }).then((r) => r.data);
53
54
  }
54
55
 
55
- // ── gzip ──────────────────────────────────────────────────────────
56
+ // ── Gzip 命名空间(一次性 + 流式)────────────────────────────────
56
57
 
57
- /** gzip 压缩(level 0-9,默认引擎默认级别)。输入 string 视为 UTF-8 文本。 */
58
- export function gzipCompress(data: Uint8Array | string, level?: number): Promise<Uint8Array> {
59
- return sendAsync<{ data: string }>('gzip.compress', { data: toB64(data), level })
60
- .then((r) => Uint8Array.fromBase64(r.data));
58
+ /** 分块压缩器:write(chunk) 该块的压缩输出(可能为空);finish() 剩余输出(含 GZIP 尾)。 */
59
+ export interface GzipCompressor {
60
+ /** 压缩一块输入,返回输出块(可能为空)。 */
61
+ write(chunk: Uint8Array | string): Promise<Uint8Array>;
62
+ /** 结束压缩,返回剩余输出(含 GZIP 尾);此后 write 不可再调用。 */
63
+ finish(): Promise<Uint8Array>;
64
+ close(): Promise<void>;
61
65
  }
62
66
 
63
- /** gzip 压缩(同步版)。 */
64
- export function gzipCompressSync(data: Uint8Array | string, level?: number): Uint8Array {
65
- return Uint8Array.fromBase64(send<{ data: string }>('gzip.compress', { data: toB64(data), level }).data);
67
+ /** 分块解压器:write(chunk) 该块可解出的输出(可能为空);finish() → 剩余输出。 */
68
+ export interface GzipDecompressor {
69
+ /** 喂入一块压缩数据,返回解压输出块(可能为空)。 */
70
+ write(chunk: Uint8Array | string): Promise<Uint8Array>;
71
+ /** 标记输入结束,返回剩余解压输出(数据不完整会 reject)。 */
72
+ finish(): Promise<Uint8Array>;
73
+ close(): Promise<void>;
66
74
  }
67
75
 
68
- /** gzip 解压(输出上限 256 MiB,超限报错——防压缩炸弹)。 */
69
- export function gzipDecompress(data: Uint8Array | string): Promise<Uint8Array> {
70
- return sendAsync<{ data: string }>('gzip.decompress', { data: toB64(data) })
71
- .then((r) => Uint8Array.fromBase64(r.data));
72
- }
76
+ export const Gzip = {
77
+ /** 一次性 gzip 压缩(level 0-9,默认引擎默认级别)。输入 string 视为 UTF-8 文本。 */
78
+ compress(data: Uint8Array | string, level?: number): Promise<Uint8Array> {
79
+ return sendAsync<{ data: string }>('gzip.compress', { data: toB64(data), level })
80
+ .then((r) => Uint8Array.fromBase64(r.data));
81
+ },
82
+ /** 一次性 gzip 压缩(同步版)。 */
83
+ compressSync(data: Uint8Array | string, level?: number): Uint8Array {
84
+ return Uint8Array.fromBase64(send<{ data: string }>('gzip.compress', { data: toB64(data), level }).data);
85
+ },
86
+ /** 一次性 gzip 解压(输出上限 256 MiB,超限报错——防压缩炸弹;上限可在 config.yml util 段调整)。 */
87
+ decompress(data: Uint8Array | string): Promise<Uint8Array> {
88
+ return sendAsync<{ data: string }>('gzip.decompress', { data: toB64(data) })
89
+ .then((r) => Uint8Array.fromBase64(r.data));
90
+ },
91
+ /** 一次性 gzip 解压(同步版)。 */
92
+ decompressSync(data: Uint8Array | string): Uint8Array {
93
+ return Uint8Array.fromBase64(send<{ data: string }>('gzip.decompress', { data: toB64(data) }).data);
94
+ },
95
+ /** 创建分块压缩器(流式管道):create → write×n → finish → close。 */
96
+ async createCompressor(options?: { level?: number }): Promise<GzipCompressor> {
97
+ const r = await sendAsync<{ id: string }>('gzip.compressor.create', { level: options?.level });
98
+ let closed = false;
99
+ const check = () => { if (closed) throw new Error('compressor closed'); };
100
+ return {
101
+ write: (chunk) => {
102
+ check();
103
+ return sendAsync<{ data: string }>('gzip.compressor.write', { id: r.id, data: toB64(chunk) })
104
+ .then((x) => Uint8Array.fromBase64(x.data));
105
+ },
106
+ finish: () => {
107
+ check();
108
+ return sendAsync<{ data: string }>('gzip.compressor.finish', { id: r.id })
109
+ .then((x) => Uint8Array.fromBase64(x.data));
110
+ },
111
+ close: async () => {
112
+ if (closed) return;
113
+ closed = true;
114
+ await sendAsync('gzip.compressor.close', { id: r.id });
115
+ },
116
+ };
117
+ },
118
+ /** 创建分块解压器(流式管道):create → write×n → finish → close。 */
119
+ async createDecompressor(): Promise<GzipDecompressor> {
120
+ const r = await sendAsync<{ id: string }>('gzip.decompressor.create', {});
121
+ let closed = false;
122
+ const check = () => { if (closed) throw new Error('decompressor closed'); };
123
+ return {
124
+ write: (chunk) => {
125
+ check();
126
+ return sendAsync<{ data: string }>('gzip.decompressor.write', { id: r.id, data: toB64(chunk) })
127
+ .then((x) => Uint8Array.fromBase64(x.data));
128
+ },
129
+ finish: () => {
130
+ check();
131
+ return sendAsync<{ data: string }>('gzip.decompressor.finish', { id: r.id })
132
+ .then((x) => Uint8Array.fromBase64(x.data));
133
+ },
134
+ close: async () => {
135
+ if (closed) return;
136
+ closed = true;
137
+ await sendAsync('gzip.decompressor.close', { id: r.id });
138
+ },
139
+ };
140
+ },
141
+ };
73
142
 
74
- /** gzip 解压(同步版)。 */
75
- export function gzipDecompressSync(data: Uint8Array | string): Uint8Array {
76
- return Uint8Array.fromBase64(send<{ data: string }>('gzip.decompress', { data: toB64(data) }).data);
77
- }