@web-ts-toolkit/express-response-handler 0.43.0 → 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/http-response.d.mts
CHANGED
|
@@ -5,16 +5,16 @@ import { OK, Created, Accepted, NonAuthoritativeInfo, NoContent, ResetContent, P
|
|
|
5
5
|
import './responses/index.mjs';
|
|
6
6
|
|
|
7
7
|
declare const HttpResponse: {
|
|
8
|
-
ok: (data:
|
|
9
|
-
created: (data:
|
|
10
|
-
accepted: (data:
|
|
11
|
-
nonAuthoritativeInfo: (data:
|
|
8
|
+
ok: <T>(data: T) => OK<T>;
|
|
9
|
+
created: <T>(data: T) => Created<T>;
|
|
10
|
+
accepted: <T>(data: T) => Accepted<T>;
|
|
11
|
+
nonAuthoritativeInfo: <T>(data: T) => NonAuthoritativeInfo<T>;
|
|
12
12
|
noContent: () => NoContent;
|
|
13
|
-
resetContent: (data:
|
|
14
|
-
partialContent: (data:
|
|
15
|
-
multiStatus: (data:
|
|
16
|
-
alreadyReported: (data:
|
|
17
|
-
imUsed: (data:
|
|
13
|
+
resetContent: <T>(data: T) => ResetContent<T>;
|
|
14
|
+
partialContent: <T>(data: T) => PartialContent<T>;
|
|
15
|
+
multiStatus: <T>(data: T) => MultiStatus<T>;
|
|
16
|
+
alreadyReported: <T>(data: T) => AlreadyReported<T>;
|
|
17
|
+
imUsed: <T>(data: T) => IMUsed<T>;
|
|
18
18
|
badRequest: (message?: string | undefined, options?: _web_ts_toolkit_http_errors.HttpErrorOptions | undefined) => BadRequestError;
|
|
19
19
|
unauthorized: (message?: string | undefined, options?: _web_ts_toolkit_http_errors.HttpErrorOptions | undefined) => UnauthorizedError;
|
|
20
20
|
forbidden: (message?: string | undefined, options?: _web_ts_toolkit_http_errors.HttpErrorOptions | undefined) => ForbiddenError;
|
|
@@ -42,7 +42,7 @@ declare const HttpResponse: {
|
|
|
42
42
|
tooManyRequests: (message?: string | undefined, options?: _web_ts_toolkit_http_errors.HttpErrorOptions | undefined) => TooManyRequestsError;
|
|
43
43
|
requestHeaderFieldsTooLarge: (message?: string | undefined, options?: _web_ts_toolkit_http_errors.HttpErrorOptions | undefined) => RequestHeaderFieldsTooLargeError;
|
|
44
44
|
unavailableForLegalReasons: (message?: string | undefined, options?: _web_ts_toolkit_http_errors.HttpErrorOptions | undefined) => UnavailableForLegalReasonsError;
|
|
45
|
-
json: (data:
|
|
45
|
+
json: <T>(data: T) => OK<T>;
|
|
46
46
|
csv: (dataset?: unknown, options?: CsvResponseOptions | undefined) => CSVResponse;
|
|
47
47
|
};
|
|
48
48
|
type HttpResponseHelpers = typeof HttpResponse;
|
package/http-response.d.ts
CHANGED
|
@@ -5,16 +5,16 @@ import { OK, Created, Accepted, NonAuthoritativeInfo, NoContent, ResetContent, P
|
|
|
5
5
|
import './responses/index.js';
|
|
6
6
|
|
|
7
7
|
declare const HttpResponse: {
|
|
8
|
-
ok: (data:
|
|
9
|
-
created: (data:
|
|
10
|
-
accepted: (data:
|
|
11
|
-
nonAuthoritativeInfo: (data:
|
|
8
|
+
ok: <T>(data: T) => OK<T>;
|
|
9
|
+
created: <T>(data: T) => Created<T>;
|
|
10
|
+
accepted: <T>(data: T) => Accepted<T>;
|
|
11
|
+
nonAuthoritativeInfo: <T>(data: T) => NonAuthoritativeInfo<T>;
|
|
12
12
|
noContent: () => NoContent;
|
|
13
|
-
resetContent: (data:
|
|
14
|
-
partialContent: (data:
|
|
15
|
-
multiStatus: (data:
|
|
16
|
-
alreadyReported: (data:
|
|
17
|
-
imUsed: (data:
|
|
13
|
+
resetContent: <T>(data: T) => ResetContent<T>;
|
|
14
|
+
partialContent: <T>(data: T) => PartialContent<T>;
|
|
15
|
+
multiStatus: <T>(data: T) => MultiStatus<T>;
|
|
16
|
+
alreadyReported: <T>(data: T) => AlreadyReported<T>;
|
|
17
|
+
imUsed: <T>(data: T) => IMUsed<T>;
|
|
18
18
|
badRequest: (message?: string | undefined, options?: _web_ts_toolkit_http_errors.HttpErrorOptions | undefined) => BadRequestError;
|
|
19
19
|
unauthorized: (message?: string | undefined, options?: _web_ts_toolkit_http_errors.HttpErrorOptions | undefined) => UnauthorizedError;
|
|
20
20
|
forbidden: (message?: string | undefined, options?: _web_ts_toolkit_http_errors.HttpErrorOptions | undefined) => ForbiddenError;
|
|
@@ -42,7 +42,7 @@ declare const HttpResponse: {
|
|
|
42
42
|
tooManyRequests: (message?: string | undefined, options?: _web_ts_toolkit_http_errors.HttpErrorOptions | undefined) => TooManyRequestsError;
|
|
43
43
|
requestHeaderFieldsTooLarge: (message?: string | undefined, options?: _web_ts_toolkit_http_errors.HttpErrorOptions | undefined) => RequestHeaderFieldsTooLargeError;
|
|
44
44
|
unavailableForLegalReasons: (message?: string | undefined, options?: _web_ts_toolkit_http_errors.HttpErrorOptions | undefined) => UnavailableForLegalReasonsError;
|
|
45
|
-
json: (data:
|
|
45
|
+
json: <T>(data: T) => OK<T>;
|
|
46
46
|
csv: (dataset?: unknown, options?: CsvResponseOptions | undefined) => CSVResponse;
|
|
47
47
|
};
|
|
48
48
|
type HttpResponseHelpers = typeof HttpResponse;
|
package/http-response.js
CHANGED
|
@@ -97,7 +97,6 @@ var IMUsed = class extends Response {
|
|
|
97
97
|
// src/responses/csv.ts
|
|
98
98
|
var import_format = require("@fast-csv/format");
|
|
99
99
|
var import_utils = require("@web-ts-toolkit/utils");
|
|
100
|
-
var import_events = require("events");
|
|
101
100
|
var CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f]/u;
|
|
102
101
|
var isIterable = (value) => typeof value === "object" && value !== null && Symbol.iterator in value;
|
|
103
102
|
var isAsyncIterable = (value) => typeof value === "object" && value !== null && Symbol.asyncIterator in value;
|
|
@@ -151,28 +150,66 @@ var CSVResponse = class {
|
|
|
151
150
|
this.headers = (0, import_utils.isPlainObject)(this.dataset);
|
|
152
151
|
}
|
|
153
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* Streams the dataset as CSV into `res`.
|
|
155
|
+
*
|
|
156
|
+
* Contract choice (B-ERH-06): minimal runtime-safe fallback, no breaking
|
|
157
|
+
* signature change. `onBeforeOutputError` stays optional.
|
|
158
|
+
*
|
|
159
|
+
* - Pre-output failure (invalid filename, failed first read, first-row
|
|
160
|
+
* processor throw, before headers are sent) with an error owner:
|
|
161
|
+
* the owner is invoked exactly once with the normalized failure and owns
|
|
162
|
+
* destination termination (the handler owner renders a single redacted
|
|
163
|
+
* JSON error; direct callers must end/destroy `res` themselves). A
|
|
164
|
+
* throwing owner is contained and the destination is then destroyed with
|
|
165
|
+
* the original failure so it never stays open.
|
|
166
|
+
* - Pre-output failure without an error owner: deterministic fallback —
|
|
167
|
+
* the destination is destroyed with the normalized failure (or ended
|
|
168
|
+
* when `destroy` is unavailable), observable via `error` + `close`.
|
|
169
|
+
* - Post-output failure (after piping starts): the destination is always
|
|
170
|
+
* destroyed with the failure, with or without an owner.
|
|
171
|
+
*
|
|
172
|
+
* Non-`Error` failures are wrapped in an `Error` with the original kept as
|
|
173
|
+
* `cause`. A throwing owner never displaces the original failure and never
|
|
174
|
+
* escapes as a process-level error.
|
|
175
|
+
*/
|
|
154
176
|
streamCsv(res, onBeforeOutputError) {
|
|
155
177
|
const stream = (0, import_format.format)({ headers: this.headers });
|
|
156
178
|
let outputStarted = false;
|
|
157
179
|
let failed = false;
|
|
158
180
|
let finished = false;
|
|
181
|
+
let cleaned = false;
|
|
159
182
|
let activeIterator = null;
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
183
|
+
const abortWaiters = /* @__PURE__ */ new Set();
|
|
184
|
+
const notifyAborted = () => {
|
|
185
|
+
for (const notify of Array.from(abortWaiters)) {
|
|
186
|
+
abortWaiters.delete(notify);
|
|
187
|
+
try {
|
|
188
|
+
notify();
|
|
189
|
+
} catch {
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
};
|
|
165
193
|
const cleanup = () => {
|
|
194
|
+
if (cleaned) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
cleaned = true;
|
|
166
198
|
stream.off("error", fail);
|
|
167
199
|
res.off?.("error", fail);
|
|
168
|
-
res.off?.("close",
|
|
169
|
-
res.off?.("finish",
|
|
200
|
+
res.off?.("close", handleClose);
|
|
201
|
+
res.off?.("finish", handleFinish);
|
|
170
202
|
};
|
|
171
203
|
const closeActiveIterator = () => {
|
|
172
204
|
const iterator = activeIterator;
|
|
173
205
|
activeIterator = null;
|
|
174
|
-
if (iterator?.return) {
|
|
206
|
+
if (!iterator?.return) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
175
210
|
void Promise.resolve(iterator.return()).catch(() => void 0);
|
|
211
|
+
} catch {
|
|
212
|
+
return;
|
|
176
213
|
}
|
|
177
214
|
};
|
|
178
215
|
const closeResponse = (error) => {
|
|
@@ -187,58 +224,129 @@ var CSVResponse = class {
|
|
|
187
224
|
return;
|
|
188
225
|
}
|
|
189
226
|
failed = true;
|
|
190
|
-
|
|
191
|
-
const normalizedError = error instanceof Error ? error :
|
|
192
|
-
|
|
227
|
+
notifyAborted();
|
|
228
|
+
const normalizedError = error instanceof Error ? error : (() => {
|
|
229
|
+
const wrapped = new Error("CSV response streaming failed");
|
|
230
|
+
try {
|
|
231
|
+
wrapped.cause = error;
|
|
232
|
+
} catch {
|
|
233
|
+
}
|
|
234
|
+
return wrapped;
|
|
235
|
+
})();
|
|
193
236
|
closeActiveIterator();
|
|
194
237
|
if (!outputStarted && !res.headersSent) {
|
|
195
|
-
|
|
238
|
+
stream.destroy();
|
|
239
|
+
if (onBeforeOutputError) {
|
|
240
|
+
cleanup();
|
|
241
|
+
try {
|
|
242
|
+
onBeforeOutputError(normalizedError);
|
|
243
|
+
} catch {
|
|
244
|
+
try {
|
|
245
|
+
res.on?.("error", () => void 0);
|
|
246
|
+
} catch {
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
closeResponse(normalizedError);
|
|
250
|
+
} catch {
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
closeResponse(normalizedError);
|
|
257
|
+
} catch {
|
|
258
|
+
cleanup();
|
|
259
|
+
}
|
|
196
260
|
return;
|
|
197
261
|
}
|
|
198
262
|
stream.destroy();
|
|
199
263
|
closeResponse(normalizedError);
|
|
200
264
|
};
|
|
201
|
-
const
|
|
202
|
-
if (!finished) {
|
|
265
|
+
const handleClose = () => {
|
|
266
|
+
if (!finished && !failed) {
|
|
203
267
|
fail(new Error("CSV response streaming aborted"));
|
|
268
|
+
cleanup();
|
|
269
|
+
return;
|
|
204
270
|
}
|
|
271
|
+
cleanup();
|
|
205
272
|
};
|
|
206
|
-
const
|
|
273
|
+
const handleFinish = () => {
|
|
207
274
|
finished = true;
|
|
275
|
+
cleanup();
|
|
208
276
|
};
|
|
209
277
|
const writeRow = async (row) => {
|
|
278
|
+
if (failed) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
210
281
|
outputStarted = true;
|
|
211
282
|
if (!stream.write(row)) {
|
|
212
|
-
await Promise
|
|
283
|
+
await new Promise((resolve) => {
|
|
284
|
+
if (failed) {
|
|
285
|
+
resolve();
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const onDrain = () => {
|
|
289
|
+
stream.off("drain", onDrain);
|
|
290
|
+
abortWaiters.delete(onAbort);
|
|
291
|
+
resolve();
|
|
292
|
+
};
|
|
293
|
+
const onAbort = () => {
|
|
294
|
+
stream.off("drain", onDrain);
|
|
295
|
+
resolve();
|
|
296
|
+
};
|
|
297
|
+
abortWaiters.add(onAbort);
|
|
298
|
+
stream.once("drain", onDrain);
|
|
299
|
+
});
|
|
213
300
|
}
|
|
214
301
|
};
|
|
215
302
|
stream.on("error", fail);
|
|
216
|
-
stream.on("end", cleanup);
|
|
217
303
|
res.on?.("error", fail);
|
|
218
|
-
res.on?.("close",
|
|
219
|
-
res.on?.("finish",
|
|
304
|
+
res.on?.("close", handleClose);
|
|
305
|
+
res.on?.("finish", handleFinish);
|
|
220
306
|
const pump = async () => {
|
|
221
307
|
try {
|
|
222
308
|
const iterator = createAsyncIterator(this.dataset);
|
|
223
309
|
activeIterator = iterator;
|
|
224
310
|
const first = await iterator.next();
|
|
225
|
-
|
|
311
|
+
if (failed) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
let firstRow;
|
|
315
|
+
if (!first.done) {
|
|
316
|
+
firstRow = this.processor(first.value);
|
|
317
|
+
if (failed) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
226
321
|
const contentDisposition = createAttachmentContentDisposition(this.filename);
|
|
322
|
+
if (failed) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
227
325
|
res.set("Content-Type", "text/csv");
|
|
228
326
|
res.set("Content-Disposition", contentDisposition);
|
|
229
327
|
stream.pipe(res);
|
|
230
|
-
if (
|
|
231
|
-
|
|
328
|
+
if (first.done) {
|
|
329
|
+
if (!failed) {
|
|
330
|
+
stream.end();
|
|
331
|
+
}
|
|
332
|
+
return;
|
|
232
333
|
}
|
|
334
|
+
await writeRow(firstRow);
|
|
233
335
|
while (!failed) {
|
|
234
|
-
const
|
|
235
|
-
if (
|
|
336
|
+
const result = await iterator.next();
|
|
337
|
+
if (failed) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (result.done) {
|
|
236
341
|
break;
|
|
237
342
|
}
|
|
238
|
-
|
|
343
|
+
const processed = this.processor(result.value);
|
|
344
|
+
if (failed) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
await writeRow(processed);
|
|
239
348
|
}
|
|
240
349
|
if (!failed) {
|
|
241
|
-
finished = true;
|
|
242
350
|
stream.end();
|
|
243
351
|
}
|
|
244
352
|
} catch (error) {
|
|
@@ -247,23 +355,23 @@ var CSVResponse = class {
|
|
|
247
355
|
activeIterator = null;
|
|
248
356
|
}
|
|
249
357
|
};
|
|
250
|
-
void pump();
|
|
358
|
+
void pump().then(void 0, () => void 0);
|
|
251
359
|
}
|
|
252
360
|
};
|
|
253
361
|
|
|
254
362
|
// src/http-response.ts
|
|
255
363
|
var import_http_errors = require("@web-ts-toolkit/http-errors");
|
|
256
364
|
var HttpResponse = {
|
|
257
|
-
ok: (
|
|
258
|
-
created: (
|
|
259
|
-
accepted: (
|
|
260
|
-
nonAuthoritativeInfo: (
|
|
365
|
+
ok: (data) => new OK(data),
|
|
366
|
+
created: (data) => new Created(data),
|
|
367
|
+
accepted: (data) => new Accepted(data),
|
|
368
|
+
nonAuthoritativeInfo: (data) => new NonAuthoritativeInfo(data),
|
|
261
369
|
noContent: (...args) => new NoContent(...args),
|
|
262
|
-
resetContent: (
|
|
263
|
-
partialContent: (
|
|
264
|
-
multiStatus: (
|
|
265
|
-
alreadyReported: (
|
|
266
|
-
imUsed: (
|
|
370
|
+
resetContent: (data) => new ResetContent(data),
|
|
371
|
+
partialContent: (data) => new PartialContent(data),
|
|
372
|
+
multiStatus: (data) => new MultiStatus(data),
|
|
373
|
+
alreadyReported: (data) => new AlreadyReported(data),
|
|
374
|
+
imUsed: (data) => new IMUsed(data),
|
|
267
375
|
badRequest: (...args) => new import_http_errors.BadRequestError(...args),
|
|
268
376
|
unauthorized: (...args) => new import_http_errors.UnauthorizedError(...args),
|
|
269
377
|
forbidden: (...args) => new import_http_errors.ForbiddenError(...args),
|
|
@@ -291,7 +399,7 @@ var HttpResponse = {
|
|
|
291
399
|
tooManyRequests: (...args) => new import_http_errors.TooManyRequestsError(...args),
|
|
292
400
|
requestHeaderFieldsTooLarge: (...args) => new import_http_errors.RequestHeaderFieldsTooLargeError(...args),
|
|
293
401
|
unavailableForLegalReasons: (...args) => new import_http_errors.UnavailableForLegalReasonsError(...args),
|
|
294
|
-
json: (
|
|
402
|
+
json: (data) => new OK(data),
|
|
295
403
|
csv: (...args) => new CSVResponse(...args)
|
|
296
404
|
};
|
|
297
405
|
// Annotate the CommonJS export names for ESM import in node:
|
package/http-response.mjs
CHANGED
|
@@ -71,7 +71,6 @@ var IMUsed = class extends Response {
|
|
|
71
71
|
// src/responses/csv.ts
|
|
72
72
|
import { format } from "@fast-csv/format";
|
|
73
73
|
import { castArray, isBoolean, isPlainObject } from "@web-ts-toolkit/utils";
|
|
74
|
-
import { once } from "events";
|
|
75
74
|
var CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f]/u;
|
|
76
75
|
var isIterable = (value) => typeof value === "object" && value !== null && Symbol.iterator in value;
|
|
77
76
|
var isAsyncIterable = (value) => typeof value === "object" && value !== null && Symbol.asyncIterator in value;
|
|
@@ -125,28 +124,66 @@ var CSVResponse = class {
|
|
|
125
124
|
this.headers = isPlainObject(this.dataset);
|
|
126
125
|
}
|
|
127
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Streams the dataset as CSV into `res`.
|
|
129
|
+
*
|
|
130
|
+
* Contract choice (B-ERH-06): minimal runtime-safe fallback, no breaking
|
|
131
|
+
* signature change. `onBeforeOutputError` stays optional.
|
|
132
|
+
*
|
|
133
|
+
* - Pre-output failure (invalid filename, failed first read, first-row
|
|
134
|
+
* processor throw, before headers are sent) with an error owner:
|
|
135
|
+
* the owner is invoked exactly once with the normalized failure and owns
|
|
136
|
+
* destination termination (the handler owner renders a single redacted
|
|
137
|
+
* JSON error; direct callers must end/destroy `res` themselves). A
|
|
138
|
+
* throwing owner is contained and the destination is then destroyed with
|
|
139
|
+
* the original failure so it never stays open.
|
|
140
|
+
* - Pre-output failure without an error owner: deterministic fallback —
|
|
141
|
+
* the destination is destroyed with the normalized failure (or ended
|
|
142
|
+
* when `destroy` is unavailable), observable via `error` + `close`.
|
|
143
|
+
* - Post-output failure (after piping starts): the destination is always
|
|
144
|
+
* destroyed with the failure, with or without an owner.
|
|
145
|
+
*
|
|
146
|
+
* Non-`Error` failures are wrapped in an `Error` with the original kept as
|
|
147
|
+
* `cause`. A throwing owner never displaces the original failure and never
|
|
148
|
+
* escapes as a process-level error.
|
|
149
|
+
*/
|
|
128
150
|
streamCsv(res, onBeforeOutputError) {
|
|
129
151
|
const stream = format({ headers: this.headers });
|
|
130
152
|
let outputStarted = false;
|
|
131
153
|
let failed = false;
|
|
132
154
|
let finished = false;
|
|
155
|
+
let cleaned = false;
|
|
133
156
|
let activeIterator = null;
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
157
|
+
const abortWaiters = /* @__PURE__ */ new Set();
|
|
158
|
+
const notifyAborted = () => {
|
|
159
|
+
for (const notify of Array.from(abortWaiters)) {
|
|
160
|
+
abortWaiters.delete(notify);
|
|
161
|
+
try {
|
|
162
|
+
notify();
|
|
163
|
+
} catch {
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
};
|
|
139
167
|
const cleanup = () => {
|
|
168
|
+
if (cleaned) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
cleaned = true;
|
|
140
172
|
stream.off("error", fail);
|
|
141
173
|
res.off?.("error", fail);
|
|
142
|
-
res.off?.("close",
|
|
143
|
-
res.off?.("finish",
|
|
174
|
+
res.off?.("close", handleClose);
|
|
175
|
+
res.off?.("finish", handleFinish);
|
|
144
176
|
};
|
|
145
177
|
const closeActiveIterator = () => {
|
|
146
178
|
const iterator = activeIterator;
|
|
147
179
|
activeIterator = null;
|
|
148
|
-
if (iterator?.return) {
|
|
180
|
+
if (!iterator?.return) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
149
184
|
void Promise.resolve(iterator.return()).catch(() => void 0);
|
|
185
|
+
} catch {
|
|
186
|
+
return;
|
|
150
187
|
}
|
|
151
188
|
};
|
|
152
189
|
const closeResponse = (error) => {
|
|
@@ -161,58 +198,129 @@ var CSVResponse = class {
|
|
|
161
198
|
return;
|
|
162
199
|
}
|
|
163
200
|
failed = true;
|
|
164
|
-
|
|
165
|
-
const normalizedError = error instanceof Error ? error :
|
|
166
|
-
|
|
201
|
+
notifyAborted();
|
|
202
|
+
const normalizedError = error instanceof Error ? error : (() => {
|
|
203
|
+
const wrapped = new Error("CSV response streaming failed");
|
|
204
|
+
try {
|
|
205
|
+
wrapped.cause = error;
|
|
206
|
+
} catch {
|
|
207
|
+
}
|
|
208
|
+
return wrapped;
|
|
209
|
+
})();
|
|
167
210
|
closeActiveIterator();
|
|
168
211
|
if (!outputStarted && !res.headersSent) {
|
|
169
|
-
|
|
212
|
+
stream.destroy();
|
|
213
|
+
if (onBeforeOutputError) {
|
|
214
|
+
cleanup();
|
|
215
|
+
try {
|
|
216
|
+
onBeforeOutputError(normalizedError);
|
|
217
|
+
} catch {
|
|
218
|
+
try {
|
|
219
|
+
res.on?.("error", () => void 0);
|
|
220
|
+
} catch {
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
closeResponse(normalizedError);
|
|
224
|
+
} catch {
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
closeResponse(normalizedError);
|
|
231
|
+
} catch {
|
|
232
|
+
cleanup();
|
|
233
|
+
}
|
|
170
234
|
return;
|
|
171
235
|
}
|
|
172
236
|
stream.destroy();
|
|
173
237
|
closeResponse(normalizedError);
|
|
174
238
|
};
|
|
175
|
-
const
|
|
176
|
-
if (!finished) {
|
|
239
|
+
const handleClose = () => {
|
|
240
|
+
if (!finished && !failed) {
|
|
177
241
|
fail(new Error("CSV response streaming aborted"));
|
|
242
|
+
cleanup();
|
|
243
|
+
return;
|
|
178
244
|
}
|
|
245
|
+
cleanup();
|
|
179
246
|
};
|
|
180
|
-
const
|
|
247
|
+
const handleFinish = () => {
|
|
181
248
|
finished = true;
|
|
249
|
+
cleanup();
|
|
182
250
|
};
|
|
183
251
|
const writeRow = async (row) => {
|
|
252
|
+
if (failed) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
184
255
|
outputStarted = true;
|
|
185
256
|
if (!stream.write(row)) {
|
|
186
|
-
await Promise
|
|
257
|
+
await new Promise((resolve) => {
|
|
258
|
+
if (failed) {
|
|
259
|
+
resolve();
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const onDrain = () => {
|
|
263
|
+
stream.off("drain", onDrain);
|
|
264
|
+
abortWaiters.delete(onAbort);
|
|
265
|
+
resolve();
|
|
266
|
+
};
|
|
267
|
+
const onAbort = () => {
|
|
268
|
+
stream.off("drain", onDrain);
|
|
269
|
+
resolve();
|
|
270
|
+
};
|
|
271
|
+
abortWaiters.add(onAbort);
|
|
272
|
+
stream.once("drain", onDrain);
|
|
273
|
+
});
|
|
187
274
|
}
|
|
188
275
|
};
|
|
189
276
|
stream.on("error", fail);
|
|
190
|
-
stream.on("end", cleanup);
|
|
191
277
|
res.on?.("error", fail);
|
|
192
|
-
res.on?.("close",
|
|
193
|
-
res.on?.("finish",
|
|
278
|
+
res.on?.("close", handleClose);
|
|
279
|
+
res.on?.("finish", handleFinish);
|
|
194
280
|
const pump = async () => {
|
|
195
281
|
try {
|
|
196
282
|
const iterator = createAsyncIterator(this.dataset);
|
|
197
283
|
activeIterator = iterator;
|
|
198
284
|
const first = await iterator.next();
|
|
199
|
-
|
|
285
|
+
if (failed) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
let firstRow;
|
|
289
|
+
if (!first.done) {
|
|
290
|
+
firstRow = this.processor(first.value);
|
|
291
|
+
if (failed) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
200
295
|
const contentDisposition = createAttachmentContentDisposition(this.filename);
|
|
296
|
+
if (failed) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
201
299
|
res.set("Content-Type", "text/csv");
|
|
202
300
|
res.set("Content-Disposition", contentDisposition);
|
|
203
301
|
stream.pipe(res);
|
|
204
|
-
if (
|
|
205
|
-
|
|
302
|
+
if (first.done) {
|
|
303
|
+
if (!failed) {
|
|
304
|
+
stream.end();
|
|
305
|
+
}
|
|
306
|
+
return;
|
|
206
307
|
}
|
|
308
|
+
await writeRow(firstRow);
|
|
207
309
|
while (!failed) {
|
|
208
|
-
const
|
|
209
|
-
if (
|
|
310
|
+
const result = await iterator.next();
|
|
311
|
+
if (failed) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (result.done) {
|
|
210
315
|
break;
|
|
211
316
|
}
|
|
212
|
-
|
|
317
|
+
const processed = this.processor(result.value);
|
|
318
|
+
if (failed) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
await writeRow(processed);
|
|
213
322
|
}
|
|
214
323
|
if (!failed) {
|
|
215
|
-
finished = true;
|
|
216
324
|
stream.end();
|
|
217
325
|
}
|
|
218
326
|
} catch (error) {
|
|
@@ -221,7 +329,7 @@ var CSVResponse = class {
|
|
|
221
329
|
activeIterator = null;
|
|
222
330
|
}
|
|
223
331
|
};
|
|
224
|
-
void pump();
|
|
332
|
+
void pump().then(void 0, () => void 0);
|
|
225
333
|
}
|
|
226
334
|
};
|
|
227
335
|
|
|
@@ -256,16 +364,16 @@ import {
|
|
|
256
364
|
UriTooLongError
|
|
257
365
|
} from "@web-ts-toolkit/http-errors";
|
|
258
366
|
var HttpResponse = {
|
|
259
|
-
ok: (
|
|
260
|
-
created: (
|
|
261
|
-
accepted: (
|
|
262
|
-
nonAuthoritativeInfo: (
|
|
367
|
+
ok: (data) => new OK(data),
|
|
368
|
+
created: (data) => new Created(data),
|
|
369
|
+
accepted: (data) => new Accepted(data),
|
|
370
|
+
nonAuthoritativeInfo: (data) => new NonAuthoritativeInfo(data),
|
|
263
371
|
noContent: (...args) => new NoContent(...args),
|
|
264
|
-
resetContent: (
|
|
265
|
-
partialContent: (
|
|
266
|
-
multiStatus: (
|
|
267
|
-
alreadyReported: (
|
|
268
|
-
imUsed: (
|
|
372
|
+
resetContent: (data) => new ResetContent(data),
|
|
373
|
+
partialContent: (data) => new PartialContent(data),
|
|
374
|
+
multiStatus: (data) => new MultiStatus(data),
|
|
375
|
+
alreadyReported: (data) => new AlreadyReported(data),
|
|
376
|
+
imUsed: (data) => new IMUsed(data),
|
|
269
377
|
badRequest: (...args) => new BadRequestError(...args),
|
|
270
378
|
unauthorized: (...args) => new UnauthorizedError(...args),
|
|
271
379
|
forbidden: (...args) => new ForbiddenError(...args),
|
|
@@ -293,7 +401,7 @@ var HttpResponse = {
|
|
|
293
401
|
tooManyRequests: (...args) => new TooManyRequestsError(...args),
|
|
294
402
|
requestHeaderFieldsTooLarge: (...args) => new RequestHeaderFieldsTooLargeError(...args),
|
|
295
403
|
unavailableForLegalReasons: (...args) => new UnavailableForLegalReasonsError(...args),
|
|
296
|
-
json: (
|
|
404
|
+
json: (data) => new OK(data),
|
|
297
405
|
csv: (...args) => new CSVResponse(...args)
|
|
298
406
|
};
|
|
299
407
|
export {
|