@web-ts-toolkit/express-response-handler 0.42.2 → 0.44.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 +29 -8
- package/create-handler.js +376 -117
- package/create-handler.mjs +376 -117
- package/http-response.d.mts +10 -10
- package/http-response.d.ts +10 -10
- package/http-response.js +146 -38
- package/http-response.mjs +146 -38
- package/index.js +376 -117
- package/index.mjs +376 -117
- package/llms.txt +21 -2
- package/package.json +28 -8
- package/public-types.d.mts +11 -2
- package/public-types.d.ts +11 -2
- package/responses/csv.d.mts +23 -0
- package/responses/csv.d.ts +23 -0
- package/responses/csv.js +136 -28
- package/responses/csv.mjs +136 -28
package/responses/csv.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// src/responses/csv.ts
|
|
2
2
|
import { format } from "@fast-csv/format";
|
|
3
3
|
import { castArray, isBoolean, isPlainObject } from "@web-ts-toolkit/utils";
|
|
4
|
-
import { once } from "events";
|
|
5
4
|
var CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f]/u;
|
|
6
5
|
var isIterable = (value) => typeof value === "object" && value !== null && Symbol.iterator in value;
|
|
7
6
|
var isAsyncIterable = (value) => typeof value === "object" && value !== null && Symbol.asyncIterator in value;
|
|
@@ -65,28 +64,66 @@ var CSVResponse = class {
|
|
|
65
64
|
this.headers = isPlainObject(this.dataset);
|
|
66
65
|
}
|
|
67
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Streams the dataset as CSV into `res`.
|
|
69
|
+
*
|
|
70
|
+
* Contract choice (B-ERH-06): minimal runtime-safe fallback, no breaking
|
|
71
|
+
* signature change. `onBeforeOutputError` stays optional.
|
|
72
|
+
*
|
|
73
|
+
* - Pre-output failure (invalid filename, failed first read, first-row
|
|
74
|
+
* processor throw, before headers are sent) with an error owner:
|
|
75
|
+
* the owner is invoked exactly once with the normalized failure and owns
|
|
76
|
+
* destination termination (the handler owner renders a single redacted
|
|
77
|
+
* JSON error; direct callers must end/destroy `res` themselves). A
|
|
78
|
+
* throwing owner is contained and the destination is then destroyed with
|
|
79
|
+
* the original failure so it never stays open.
|
|
80
|
+
* - Pre-output failure without an error owner: deterministic fallback —
|
|
81
|
+
* the destination is destroyed with the normalized failure (or ended
|
|
82
|
+
* when `destroy` is unavailable), observable via `error` + `close`.
|
|
83
|
+
* - Post-output failure (after piping starts): the destination is always
|
|
84
|
+
* destroyed with the failure, with or without an owner.
|
|
85
|
+
*
|
|
86
|
+
* Non-`Error` failures are wrapped in an `Error` with the original kept as
|
|
87
|
+
* `cause`. A throwing owner never displaces the original failure and never
|
|
88
|
+
* escapes as a process-level error.
|
|
89
|
+
*/
|
|
68
90
|
streamCsv(res, onBeforeOutputError) {
|
|
69
91
|
const stream = format({ headers: this.headers });
|
|
70
92
|
let outputStarted = false;
|
|
71
93
|
let failed = false;
|
|
72
94
|
let finished = false;
|
|
95
|
+
let cleaned = false;
|
|
73
96
|
let activeIterator = null;
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
97
|
+
const abortWaiters = /* @__PURE__ */ new Set();
|
|
98
|
+
const notifyAborted = () => {
|
|
99
|
+
for (const notify of Array.from(abortWaiters)) {
|
|
100
|
+
abortWaiters.delete(notify);
|
|
101
|
+
try {
|
|
102
|
+
notify();
|
|
103
|
+
} catch {
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
79
107
|
const cleanup = () => {
|
|
108
|
+
if (cleaned) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
cleaned = true;
|
|
80
112
|
stream.off("error", fail);
|
|
81
113
|
res.off?.("error", fail);
|
|
82
|
-
res.off?.("close",
|
|
83
|
-
res.off?.("finish",
|
|
114
|
+
res.off?.("close", handleClose);
|
|
115
|
+
res.off?.("finish", handleFinish);
|
|
84
116
|
};
|
|
85
117
|
const closeActiveIterator = () => {
|
|
86
118
|
const iterator = activeIterator;
|
|
87
119
|
activeIterator = null;
|
|
88
|
-
if (iterator?.return) {
|
|
120
|
+
if (!iterator?.return) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
89
124
|
void Promise.resolve(iterator.return()).catch(() => void 0);
|
|
125
|
+
} catch {
|
|
126
|
+
return;
|
|
90
127
|
}
|
|
91
128
|
};
|
|
92
129
|
const closeResponse = (error) => {
|
|
@@ -101,58 +138,129 @@ var CSVResponse = class {
|
|
|
101
138
|
return;
|
|
102
139
|
}
|
|
103
140
|
failed = true;
|
|
104
|
-
|
|
105
|
-
const normalizedError = error instanceof Error ? error :
|
|
106
|
-
|
|
141
|
+
notifyAborted();
|
|
142
|
+
const normalizedError = error instanceof Error ? error : (() => {
|
|
143
|
+
const wrapped = new Error("CSV response streaming failed");
|
|
144
|
+
try {
|
|
145
|
+
wrapped.cause = error;
|
|
146
|
+
} catch {
|
|
147
|
+
}
|
|
148
|
+
return wrapped;
|
|
149
|
+
})();
|
|
107
150
|
closeActiveIterator();
|
|
108
151
|
if (!outputStarted && !res.headersSent) {
|
|
109
|
-
|
|
152
|
+
stream.destroy();
|
|
153
|
+
if (onBeforeOutputError) {
|
|
154
|
+
cleanup();
|
|
155
|
+
try {
|
|
156
|
+
onBeforeOutputError(normalizedError);
|
|
157
|
+
} catch {
|
|
158
|
+
try {
|
|
159
|
+
res.on?.("error", () => void 0);
|
|
160
|
+
} catch {
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
closeResponse(normalizedError);
|
|
164
|
+
} catch {
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
closeResponse(normalizedError);
|
|
171
|
+
} catch {
|
|
172
|
+
cleanup();
|
|
173
|
+
}
|
|
110
174
|
return;
|
|
111
175
|
}
|
|
112
176
|
stream.destroy();
|
|
113
177
|
closeResponse(normalizedError);
|
|
114
178
|
};
|
|
115
|
-
const
|
|
116
|
-
if (!finished) {
|
|
179
|
+
const handleClose = () => {
|
|
180
|
+
if (!finished && !failed) {
|
|
117
181
|
fail(new Error("CSV response streaming aborted"));
|
|
182
|
+
cleanup();
|
|
183
|
+
return;
|
|
118
184
|
}
|
|
185
|
+
cleanup();
|
|
119
186
|
};
|
|
120
|
-
const
|
|
187
|
+
const handleFinish = () => {
|
|
121
188
|
finished = true;
|
|
189
|
+
cleanup();
|
|
122
190
|
};
|
|
123
191
|
const writeRow = async (row) => {
|
|
192
|
+
if (failed) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
124
195
|
outputStarted = true;
|
|
125
196
|
if (!stream.write(row)) {
|
|
126
|
-
await Promise
|
|
197
|
+
await new Promise((resolve) => {
|
|
198
|
+
if (failed) {
|
|
199
|
+
resolve();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const onDrain = () => {
|
|
203
|
+
stream.off("drain", onDrain);
|
|
204
|
+
abortWaiters.delete(onAbort);
|
|
205
|
+
resolve();
|
|
206
|
+
};
|
|
207
|
+
const onAbort = () => {
|
|
208
|
+
stream.off("drain", onDrain);
|
|
209
|
+
resolve();
|
|
210
|
+
};
|
|
211
|
+
abortWaiters.add(onAbort);
|
|
212
|
+
stream.once("drain", onDrain);
|
|
213
|
+
});
|
|
127
214
|
}
|
|
128
215
|
};
|
|
129
216
|
stream.on("error", fail);
|
|
130
|
-
stream.on("end", cleanup);
|
|
131
217
|
res.on?.("error", fail);
|
|
132
|
-
res.on?.("close",
|
|
133
|
-
res.on?.("finish",
|
|
218
|
+
res.on?.("close", handleClose);
|
|
219
|
+
res.on?.("finish", handleFinish);
|
|
134
220
|
const pump = async () => {
|
|
135
221
|
try {
|
|
136
222
|
const iterator = createAsyncIterator(this.dataset);
|
|
137
223
|
activeIterator = iterator;
|
|
138
224
|
const first = await iterator.next();
|
|
139
|
-
|
|
225
|
+
if (failed) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
let firstRow;
|
|
229
|
+
if (!first.done) {
|
|
230
|
+
firstRow = this.processor(first.value);
|
|
231
|
+
if (failed) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
140
235
|
const contentDisposition = createAttachmentContentDisposition(this.filename);
|
|
236
|
+
if (failed) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
141
239
|
res.set("Content-Type", "text/csv");
|
|
142
240
|
res.set("Content-Disposition", contentDisposition);
|
|
143
241
|
stream.pipe(res);
|
|
144
|
-
if (
|
|
145
|
-
|
|
242
|
+
if (first.done) {
|
|
243
|
+
if (!failed) {
|
|
244
|
+
stream.end();
|
|
245
|
+
}
|
|
246
|
+
return;
|
|
146
247
|
}
|
|
248
|
+
await writeRow(firstRow);
|
|
147
249
|
while (!failed) {
|
|
148
|
-
const
|
|
149
|
-
if (
|
|
250
|
+
const result = await iterator.next();
|
|
251
|
+
if (failed) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (result.done) {
|
|
150
255
|
break;
|
|
151
256
|
}
|
|
152
|
-
|
|
257
|
+
const processed = this.processor(result.value);
|
|
258
|
+
if (failed) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
await writeRow(processed);
|
|
153
262
|
}
|
|
154
263
|
if (!failed) {
|
|
155
|
-
finished = true;
|
|
156
264
|
stream.end();
|
|
157
265
|
}
|
|
158
266
|
} catch (error) {
|
|
@@ -161,7 +269,7 @@ var CSVResponse = class {
|
|
|
161
269
|
activeIterator = null;
|
|
162
270
|
}
|
|
163
271
|
};
|
|
164
|
-
void pump();
|
|
272
|
+
void pump().then(void 0, () => void 0);
|
|
165
273
|
}
|
|
166
274
|
};
|
|
167
275
|
export {
|