编码母题 / P1 / Node 与 JavaScript / 04
支持背压的流式数据管道
先读清题意与边界,独立尝试,再展开思路并对照可运行的参考代码。
重要程度高价值难度困难预计完成45–60 分钟
题目是什么
把异步可迭代源持续写入 Node Writable。write 返回 false 时暂停生产者等待 drain;还要处理 error、取消和正常 finish。
输入输出示例
写入触发背压后,只有 drain 到达才读取并写入下一块。
练习起手式与验证
复制代码后补全空方法,再运行下方三组验证;复杂题所需的辅助类型与输入准备一并放在代码块中。
/*
* 题目:支持背压的流式数据管道
* 要求:把异步可迭代源持续写入 Node Writable。write 返回 false
* 时暂停生产者等待 drain;还要处理 error、取消和正常 finish。
* 示例:写入触发背压后,只有 drain 到达才读取并写入下一块。
*/
const { Writable } = require("node:stream");
function waitEvent(stream, successEvent, signal) {}
async function writeWithBackpressure(source, writable, { signal } = {}) {}
(async () => {
const chunks = [];
let writes = 0;
const writable = new Writable({
highWaterMark: 1,
write(chunk, _encoding, callback) {
writes += 1;
setTimeout(() => {
chunks.push(chunk.toString());
callback();
}, 2);
},
});
async function* source() {
yield "a";
yield "b";
yield "c";
}
await writeWithBackpressure(source(), writable);
console.log(writes); // 预期:3
console.log(chunks); // 预期:["a", "b", "c"]
console.log(writable.writableFinished); // 预期:true
})().catch(console.error);
/*
* 题目:支持背压的流式数据管道
* 要求:把异步可迭代源持续写入 Node Writable。write 返回 false
* 时暂停生产者等待 drain;还要处理 error、取消和正常 finish。
* 示例:写入触发背压后,只有 drain 到达才读取并写入下一块。
*/
const { Writable } = require("node:stream");
function waitEvent(stream, successEvent, signal) {}
async function writeWithBackpressure(source, writable, { signal } = {}) {}
(async () => {
const chunks = [];
let writes = 0;
const writable = new Writable({
highWaterMark: 1,
write(chunk, _encoding, callback) {
writes += 1;
setTimeout(() => {
chunks.push(chunk.toString());
callback();
}, 2);
},
});
async function* source() {
yield "a";
yield "b";
yield "c";
}
await writeWithBackpressure(source(), writable);
console.log(writes); // 预期:3
console.log(chunks); // 预期:["a", "b", "c"]
console.log(writable.writableFinished); // 预期:true
})().catch(console.error);解题思路
展开思路、不变量与常见边界
关键切入点:for await 消费源,遇到 write=false 等待 drain/error/abort 之一,并在所有分支清理监听器。
核心模型:for await 消费源;write false 后 await drain/error/abort 三者之一; 正常结束调用 end 并等待 finish。监听必须在任一分支完成后清理。 不变量:收到 drain 前不再 write;失败/取消后不继续消费;函数 resolve 时输出 已 finish。迁移:文件、HTTP 响应、SSE、模型 Token 和批量数据导出。
参考代码与验证
“关键机制”注释用于解释实现,下载链接提供未经页面注释修改的原始 JS 文件。
const { Writable } = require("node:stream");
// 关键机制:for await 消费源,遇到 write=false 等待 drain/error/abort 之一,并在所有分支清理监听器。
function waitEvent(stream, successEvent, signal) {
if (signal?.aborted)
return Promise.reject(signal.reason || new Error("aborted"));
return new Promise((resolve, reject) => {
const onSuccess = () => {
cleanup();
resolve();
};
const onError = (e) => {
cleanup();
reject(e);
};
const onAbort = () => {
cleanup();
reject(signal.reason || new Error("aborted"));
};
function cleanup() {
stream.off(successEvent, onSuccess);
stream.off("error", onError);
signal?.removeEventListener("abort", onAbort);
}
stream.once(successEvent, onSuccess);
stream.once("error", onError);
signal?.addEventListener("abort", onAbort, { once: true });
});
}
async function writeWithBackpressure(source, writable, { signal } = {}) {
try {
for await (const chunk of source) {
if (signal?.aborted) throw signal.reason || new Error("aborted");
if (!writable.write(chunk)) await waitEvent(writable, "drain", signal);
}
const finished = waitEvent(writable, "finish", signal);
writable.end();
await finished;
} catch (error) {
if (!writable.destroyed) writable.destroy();
throw error;
}
}
// 三组以上可直接运行的验证;异步题使用独立执行块。
(async () => {
const chunks = [];
let writes = 0;
const writable = new Writable({
highWaterMark: 1,
write(chunk, _encoding, callback) {
writes += 1;
setTimeout(() => {
chunks.push(chunk.toString());
callback();
}, 2);
},
});
async function* source() {
yield "a";
yield "b";
yield "c";
}
await writeWithBackpressure(source(), writable);
console.log(writes); // 预期:3
console.log(chunks); // 预期:["a", "b", "c"]
console.log(writable.writableFinished); // 预期:true
})().catch(console.error);
const { Writable } = require("node:stream");
// 关键机制:for await 消费源,遇到 write=false 等待 drain/error/abort 之一,并在所有分支清理监听器。
function waitEvent(stream, successEvent, signal) {
if (signal?.aborted)
return Promise.reject(signal.reason || new Error("aborted"));
return new Promise((resolve, reject) => {
const onSuccess = () => {
cleanup();
resolve();
};
const onError = (e) => {
cleanup();
reject(e);
};
const onAbort = () => {
cleanup();
reject(signal.reason || new Error("aborted"));
};
function cleanup() {
stream.off(successEvent, onSuccess);
stream.off("error", onError);
signal?.removeEventListener("abort", onAbort);
}
stream.once(successEvent, onSuccess);
stream.once("error", onError);
signal?.addEventListener("abort", onAbort, { once: true });
});
}
async function writeWithBackpressure(source, writable, { signal } = {}) {
try {
for await (const chunk of source) {
if (signal?.aborted) throw signal.reason || new Error("aborted");
if (!writable.write(chunk)) await waitEvent(writable, "drain", signal);
}
const finished = waitEvent(writable, "finish", signal);
writable.end();
await finished;
} catch (error) {
if (!writable.destroyed) writable.destroy();
throw error;
}
}
// 三组以上可直接运行的验证;异步题使用独立执行块。
(async () => {
const chunks = [];
let writes = 0;
const writable = new Writable({
highWaterMark: 1,
write(chunk, _encoding, callback) {
writes += 1;
setTimeout(() => {
chunks.push(chunk.toString());
callback();
}, 2);
},
});
async function* source() {
yield "a";
yield "b";
yield "c";
}
await writeWithBackpressure(source(), writable);
console.log(writes); // 预期:3
console.log(chunks); // 预期:["a", "b", "c"]
console.log(writable.writableFinished); // 预期:true
})().catch(console.error);
复杂度分析
时间复杂度:O(n),不计底层 I/O。从源读取并向目标写入 n 个数据块各一次。
空间复杂度:O(1) 辅助空间。只等待当前写入和 drain;流自身的缓冲空间另计。