@blamejs/blamejs-shop 0.5.19 → 0.5.21
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/CHANGELOG.md +4 -0
- package/README.md +1 -0
- package/SECURITY.md +17 -0
- package/lib/addresses.js +2 -2
- package/lib/admin.js +49 -45
- package/lib/affiliates.js +7 -23
- package/lib/announcement-bar.js +5 -22
- package/lib/api-keys.js +4 -13
- package/lib/asset-manifest.json +1 -1
- package/lib/banner-ab-tests.js +5 -9
- package/lib/blog-articles.js +8 -26
- package/lib/captcha-gate.js +7 -17
- package/lib/carrier-accounts.js +3 -10
- package/lib/catalog-drafts.js +5 -12
- package/lib/category-navigation.js +4 -20
- package/lib/cms-blocks.js +4 -7
- package/lib/compliance-export.js +4 -11
- package/lib/consent-ledger.js +2 -4
- package/lib/cost-layers.js +56 -21
- package/lib/customer-import.js +2 -2
- package/lib/customer-notes.js +7 -23
- package/lib/customer-roles.js +3 -5
- package/lib/customer-segments.js +17 -14
- package/lib/customer-surveys.js +7 -14
- package/lib/customers.js +2 -4
- package/lib/dispute-resolution.js +5 -13
- package/lib/dropship-forwarding.js +3 -5
- package/lib/dunning.js +2 -7
- package/lib/email-ab-tests.js +4 -8
- package/lib/email-campaigns.js +25 -14
- package/lib/email-warmup.js +2 -7
- package/lib/experiments.js +6 -10
- package/lib/externaldb-d1.js +78 -44
- package/lib/geolocation.js +18 -28
- package/lib/gift-options.js +5 -23
- package/lib/knowledge-base.js +7 -22
- package/lib/line-gift-wrap.js +3 -17
- package/lib/live-chat.js +5 -18
- package/lib/metered-usage.js +3 -8
- package/lib/notifications.js +3 -7
- package/lib/operator-accounts.js +3 -5
- package/lib/operator-approvals.js +6 -8
- package/lib/operator-audit-log.js +2 -7
- package/lib/operator-help-center.js +5 -21
- package/lib/operator-inbox.js +4 -11
- package/lib/operator-roles.js +5 -7
- package/lib/order-escalation.js +2 -2
- package/lib/order-export.js +24 -20
- package/lib/order-notes.js +4 -14
- package/lib/order-ratings.js +4 -10
- package/lib/payment-methods.js +5 -13
- package/lib/payment-retries.js +2 -7
- package/lib/payment.js +103 -58
- package/lib/pixel-events.js +8 -23
- package/lib/plan-changes.js +2 -7
- package/lib/product-qa.js +4 -8
- package/lib/promo-banners.js +7 -15
- package/lib/purchase-orders.js +3 -6
- package/lib/push-notifications.js +7 -32
- package/lib/pwa-manifest.js +6 -11
- package/lib/quotes.js +5 -9
- package/lib/referrals.js +10 -6
- package/lib/reviews.js +4 -10
- package/lib/robots-config.js +6 -14
- package/lib/sales-tax-filings.js +2 -2
- package/lib/search-facets.js +5 -5
- package/lib/search-ranking.js +4 -7
- package/lib/search-synonyms.js +7 -21
- package/lib/security-middleware.js +125 -70
- package/lib/seller-signup.js +10 -26
- package/lib/sidebar-widgets.js +6 -15
- package/lib/site-redirects.js +49 -137
- package/lib/sitemap-generator.js +3 -7
- package/lib/sms-dispatcher.js +7 -23
- package/lib/stock-receipts.js +4 -10
- package/lib/storefront-dashboards.js +2 -10
- package/lib/storefront-forms.js +9 -22
- package/lib/storefront-pages.js +6 -20
- package/lib/storefront.js +21 -23
- package/lib/subscription-billing.js +3 -8
- package/lib/subscription-controls.js +2 -7
- package/lib/suggestion-box.js +9 -26
- package/lib/support-tickets.js +6 -24
- package/lib/tax-remittance.js +2 -2
- package/lib/tenants.js +2 -7
- package/lib/text-guard.js +137 -35
- package/lib/theme-assets.js +4 -16
- package/lib/translations.js +3 -6
- package/lib/trust-badges.js +4 -7
- package/lib/vendor/MANIFEST.json +107 -97
- package/lib/vendor/blamejs/CHANGELOG.md +180 -0
- package/lib/vendor/blamejs/NOTICE +14 -10
- package/lib/vendor/blamejs/README.md +17 -5
- package/lib/vendor/blamejs/index.js +2 -0
- package/lib/vendor/blamejs/lib/acme.js +9 -9
- package/lib/vendor/blamejs/lib/api-snapshot.js +35 -16
- package/lib/vendor/blamejs/lib/app-shutdown.js +1 -1
- package/lib/vendor/blamejs/lib/archive-adapters.js +1 -1
- package/lib/vendor/blamejs/lib/archive.js +13 -24
- package/lib/vendor/blamejs/lib/auth/access-lock.js +2 -1
- package/lib/vendor/blamejs/lib/auth/bot-challenge.js +3 -6
- package/lib/vendor/blamejs/lib/auth/ciba.js +32 -3
- package/lib/vendor/blamejs/lib/auth/oauth.js +65 -8
- package/lib/vendor/blamejs/lib/auth/password.js +1 -1
- package/lib/vendor/blamejs/lib/auth/saml.js +21 -3
- package/lib/vendor/blamejs/lib/auth-bot-challenge.js +3 -3
- package/lib/vendor/blamejs/lib/backup/index.js +5 -5
- package/lib/vendor/blamejs/lib/case-fold-classes.js +491 -0
- package/lib/vendor/blamejs/lib/codepoint-class.js +131 -0
- package/lib/vendor/blamejs/lib/constants.js +79 -0
- package/lib/vendor/blamejs/lib/db-declare-view.js +8 -2
- package/lib/vendor/blamejs/lib/error-page.js +20 -1
- package/lib/vendor/blamejs/lib/external-db.js +5 -1
- package/lib/vendor/blamejs/lib/flag-evaluation-context.js +24 -6
- package/lib/vendor/blamejs/lib/flag.js +28 -2
- package/lib/vendor/blamejs/lib/framework-schema.js +14 -3
- package/lib/vendor/blamejs/lib/guard-regex.js +2345 -89
- package/lib/vendor/blamejs/lib/http-client.js +217 -40
- package/lib/vendor/blamejs/lib/mail-bimi.js +1 -1
- package/lib/vendor/blamejs/lib/mail-crypto-pgp.js +3 -2
- package/lib/vendor/blamejs/lib/mail-dav.js +26 -26
- package/lib/vendor/blamejs/lib/mail-deploy.js +10 -10
- package/lib/vendor/blamejs/lib/mail-scan.js +3 -0
- package/lib/vendor/blamejs/lib/mail-store.js +7 -1
- package/lib/vendor/blamejs/lib/mail.js +5 -1
- package/lib/vendor/blamejs/lib/middleware/age-gate.js +3 -2
- package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +1 -1
- package/lib/vendor/blamejs/lib/middleware/assetlinks.js +2 -1
- package/lib/vendor/blamejs/lib/middleware/asyncapi-serve.js +3 -2
- package/lib/vendor/blamejs/lib/middleware/bearer-auth.js +2 -1
- package/lib/vendor/blamejs/lib/middleware/compression.js +1 -1
- package/lib/vendor/blamejs/lib/middleware/cors.js +1 -1
- package/lib/vendor/blamejs/lib/middleware/csp-report.js +5 -5
- package/lib/vendor/blamejs/lib/middleware/csrf-protect.js +2 -1
- package/lib/vendor/blamejs/lib/middleware/daily-byte-quota.js +2 -2
- package/lib/vendor/blamejs/lib/middleware/deny-response.js +2 -1
- package/lib/vendor/blamejs/lib/middleware/dpop.js +2 -2
- package/lib/vendor/blamejs/lib/middleware/fetch-metadata.js +2 -1
- package/lib/vendor/blamejs/lib/middleware/flag-context.js +17 -0
- package/lib/vendor/blamejs/lib/middleware/idempotency-key.js +10 -8
- package/lib/vendor/blamejs/lib/middleware/openapi-serve.js +3 -2
- package/lib/vendor/blamejs/lib/middleware/require-aal.js +3 -2
- package/lib/vendor/blamejs/lib/middleware/require-auth.js +4 -3
- package/lib/vendor/blamejs/lib/middleware/require-bound-key.js +4 -3
- package/lib/vendor/blamejs/lib/middleware/require-content-type.js +3 -2
- package/lib/vendor/blamejs/lib/middleware/require-methods.js +3 -2
- package/lib/vendor/blamejs/lib/middleware/require-mtls.js +3 -2
- package/lib/vendor/blamejs/lib/middleware/security-txt.js +2 -1
- package/lib/vendor/blamejs/lib/middleware/web-app-manifest.js +2 -1
- package/lib/vendor/blamejs/lib/network-dns-resolver.js +1 -1
- package/lib/vendor/blamejs/lib/network-dns.js +3 -3
- package/lib/vendor/blamejs/lib/network-nts.js +35 -14
- package/lib/vendor/blamejs/lib/network-proxy.js +42 -4
- package/lib/vendor/blamejs/lib/network-smtp-policy.js +1 -1
- package/lib/vendor/blamejs/lib/network-tls.js +341 -11
- package/lib/vendor/blamejs/lib/nist-crosswalk.js +2 -2
- package/lib/vendor/blamejs/lib/ntp-check.js +30 -19
- package/lib/vendor/blamejs/lib/object-store/azure-blob.js +1 -1
- package/lib/vendor/blamejs/lib/object-store/gcs.js +1 -1
- package/lib/vendor/blamejs/lib/object-store/http-put.js +2 -1
- package/lib/vendor/blamejs/lib/object-store/sigv4-bucket-ops.js +5 -5
- package/lib/vendor/blamejs/lib/object-store/sigv4.js +2 -2
- package/lib/vendor/blamejs/lib/observability-otlp-exporter.js +4 -2
- package/lib/vendor/blamejs/lib/openapi.js +1 -1
- package/lib/vendor/blamejs/lib/pqc-agent.js +4 -7
- package/lib/vendor/blamejs/lib/problem-details.js +2 -1
- package/lib/vendor/blamejs/lib/redis-client.js +12 -1
- package/lib/vendor/blamejs/lib/regex-linear.js +1349 -0
- package/lib/vendor/blamejs/lib/render.js +531 -7
- package/lib/vendor/blamejs/lib/request-helpers.js +181 -5
- package/lib/vendor/blamejs/lib/router.js +37 -13
- package/lib/vendor/blamejs/lib/safe-async.js +142 -0
- package/lib/vendor/blamejs/lib/safe-icap.js +3 -0
- package/lib/vendor/blamejs/lib/safe-sql.js +15 -3
- package/lib/vendor/blamejs/lib/self-update.js +2 -2
- package/lib/vendor/blamejs/lib/testing.js +54 -7
- package/lib/vendor/blamejs/lib/validate-opts.js +56 -0
- package/lib/vendor/blamejs/lib/vendor/MANIFEST.json +62 -25
- package/lib/vendor/blamejs/lib/vendor/blamejs-pki.cjs +2004 -142
- package/lib/vendor/blamejs/lib/vendor/browser/noble-ciphers.mjs +727 -0
- package/lib/vendor/blamejs/lib/vendor/browser/noble-hashes.mjs +1070 -0
- package/lib/vendor/blamejs/lib/vendor/browser/noble-post-quantum.mjs +1090 -0
- package/lib/vendor/blamejs/lib/vendor/noble-post-quantum.cjs +510 -227
- package/lib/vendor/blamejs/lib/webhook-hmac.js +19 -2
- package/lib/vendor/blamejs/lib/webhook.js +1 -1
- package/lib/vendor/blamejs/lib/ws-client.js +24 -2
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/sbom.cdx.json +6 -6
- package/lib/vendor-invoices.js +3 -6
- package/lib/vendors.js +5 -19
- package/lib/webhook-receiver.js +46 -83
- package/lib/webhook-subscriptions.js +3 -13
- package/lib/wishlist-sharing.js +5 -10
- package/package.json +1 -1
|
@@ -30,16 +30,41 @@
|
|
|
30
30
|
* Server-side HTML / JSON / XML response helpers.
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
|
+
var C = require("./constants");
|
|
34
|
+
var lazyRequire = require("./lazy-require");
|
|
33
35
|
var validateOpts = require("./validate-opts");
|
|
34
36
|
|
|
37
|
+
// safe-async — lazy because render is required during boot by the router and
|
|
38
|
+
// only the streaming helper needs it, so an operator who never streams does
|
|
39
|
+
// not pull the async toolkit in at load.
|
|
40
|
+
var safeAsync = lazyRequire(function () { return require("./safe-async"); });
|
|
41
|
+
// request-helpers — lazy for the same reason, and because it requires render's
|
|
42
|
+
// siblings; only the streaming and error paths need it.
|
|
43
|
+
var requestHelpers = lazyRequire(function () { return require("./request-helpers"); });
|
|
44
|
+
|
|
35
45
|
var DEFAULT_CHARSET = "utf-8";
|
|
36
46
|
|
|
37
47
|
function _alreadyDone(res) {
|
|
38
48
|
return res && res.writableEnded === true;
|
|
39
49
|
}
|
|
40
50
|
|
|
51
|
+
// The status to send. A status that was NOT given takes the default; one that
|
|
52
|
+
// was is sent exactly as given, so `writeHead` reports a nonsense value rather
|
|
53
|
+
// than this quietly turning it into a 200. `0` and `NaN` are the pair that
|
|
54
|
+
// makes the difference: both are falsy, and defaulting them away would answer
|
|
55
|
+
// a misconfigured handler with a successful response.
|
|
56
|
+
function _statusOr(opts, fallback) {
|
|
57
|
+
if (!opts || opts.status === undefined || opts.status === null) return fallback;
|
|
58
|
+
return opts.status;
|
|
59
|
+
}
|
|
60
|
+
|
|
41
61
|
function _writeResponse(res, status, headers, body) {
|
|
42
62
|
if (_alreadyDone(res)) return;
|
|
63
|
+
// The headers are already on the wire — writeHead would throw
|
|
64
|
+
// ERR_HTTP_HEADERS_SENT and take the caller's error path with it. This is
|
|
65
|
+
// the natural recovery after a streaming failure ("catch, then render a
|
|
66
|
+
// 500"), so it has to fail as an incomplete transfer rather than a crash.
|
|
67
|
+
if (res.headersSent === true && requestHelpers().failAfterHeaders(res)) return;
|
|
43
68
|
if (typeof res.writeHead === "function") {
|
|
44
69
|
res.writeHead(status, headers);
|
|
45
70
|
} else {
|
|
@@ -62,6 +87,43 @@ function _mergedHeaders(base, extra) {
|
|
|
62
87
|
return out;
|
|
63
88
|
}
|
|
64
89
|
|
|
90
|
+
// `writeHead(status, headers)` merges with what `setHeader` recorded but lets
|
|
91
|
+
// its own object win on a name they share, so a default here replaced whatever a
|
|
92
|
+
// route had already said — silently, and only for the names the defaults happen
|
|
93
|
+
// to carry, so a `Content-Disposition` survived while the `Content-Type` beside
|
|
94
|
+
// it did not.
|
|
95
|
+
//
|
|
96
|
+
// Exactly one default steps aside for that, and only in `stream`. The reason is
|
|
97
|
+
// what the header MEANS in each place. `json`, `text` and `htmlString` encode
|
|
98
|
+
// the body themselves, so their `Content-Type` describes the bytes they just
|
|
99
|
+
// produced — it belongs with `Content-Length`, not with a preference, and
|
|
100
|
+
// inheriting an earlier one is how a JSON error body comes to be served as
|
|
101
|
+
// `text/html` and a reflected value in it becomes markup. `stream` does not know
|
|
102
|
+
// what its bytes are: `application/octet-stream` is a placeholder for an answer
|
|
103
|
+
// the caller has, which is exactly the case the route stating `text/csv` was
|
|
104
|
+
// giving it.
|
|
105
|
+
//
|
|
106
|
+
// `Cache-Control` never steps aside either, in any of them. It is a security
|
|
107
|
+
// default rather than a formatting one — dynamic responses must revalidate —
|
|
108
|
+
// and a default that any earlier `setHeader` in the chain could relax would not
|
|
109
|
+
// be a default. `opts.headers` remains the way to say otherwise, deliberately,
|
|
110
|
+
// because that says it at the call rather than somewhere up the middleware.
|
|
111
|
+
function _defaultContentTypeUnlessStated(res, defaults) {
|
|
112
|
+
if (!res || typeof res.getHeader !== "function") return defaults;
|
|
113
|
+
var stated = res.getHeader("Content-Type");
|
|
114
|
+
// A header set to nothing states nothing. `setHeader` also accepts an ARRAY,
|
|
115
|
+
// and a wrapper that keeps every header that way would have looked like it
|
|
116
|
+
// had said nothing at all.
|
|
117
|
+
if (Array.isArray(stated)) stated = stated.length ? stated[0] : "";
|
|
118
|
+
if (typeof stated === "number") stated = String(stated);
|
|
119
|
+
if (typeof stated !== "string" || stated === "") return defaults;
|
|
120
|
+
var out = {};
|
|
121
|
+
Object.keys(defaults).forEach(function (name) {
|
|
122
|
+
if (name !== "Content-Type") out[name] = defaults[name];
|
|
123
|
+
});
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
|
|
65
127
|
// Default Cache-Control for dynamic responses. Browsers heuristically
|
|
66
128
|
// cache HTML responses without explicit headers, which causes "saved
|
|
67
129
|
// changes don't appear" bugs after a POST/redirect. `no-cache` permits
|
|
@@ -83,8 +145,13 @@ var DEFAULT_DYNAMIC_CACHE_CONTROL = "private, no-cache, must-revalidate";
|
|
|
83
145
|
* and the dynamic-response Cache-Control. Status defaults to 200;
|
|
84
146
|
* any custom headers in `opts.headers` merge over the defaults so
|
|
85
147
|
* operators can pin a different Cache-Control or add CORS headers
|
|
86
|
-
* without losing Content-Type.
|
|
87
|
-
*
|
|
148
|
+
* without losing Content-Type. The Content-Type it sends is its own:
|
|
149
|
+
* this helper encodes the body, so the type describes the bytes it
|
|
150
|
+
* produced rather than a preference, and a `text/html` left on the
|
|
151
|
+
* response by an earlier `res.setHeader` does not carry over — that
|
|
152
|
+
* is how a JSON error body comes to be parsed as markup. Say it in
|
|
153
|
+
* `opts.headers` to send something else. Returns `undefined` — the
|
|
154
|
+
* response is fully written by the time the call returns.
|
|
88
155
|
*
|
|
89
156
|
* `opts.replacer` is forwarded to `JSON.stringify` (ECMA-262 §25.5.2,
|
|
90
157
|
* the second argument) so handlers can serialize values that have no
|
|
@@ -120,7 +187,462 @@ function json(res, body, opts) {
|
|
|
120
187
|
"Content-Length": Buffer.byteLength(encoded, "utf8"),
|
|
121
188
|
"Cache-Control": DEFAULT_DYNAMIC_CACHE_CONTROL,
|
|
122
189
|
}, opts.headers);
|
|
123
|
-
_writeResponse(res, opts.
|
|
190
|
+
_writeResponse(res, _statusOr(opts, C.HTTP.STATUS.OK), headers, encoded);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* @primitive b.render.stream
|
|
195
|
+
* @signature b.render.stream(res, iterable, opts?)
|
|
196
|
+
* @since 0.18.19
|
|
197
|
+
* @status stable
|
|
198
|
+
* @related b.render.json, b.safeAsync.writeChunk
|
|
199
|
+
*
|
|
200
|
+
* Write an async (or sync) iterable to a response, one chunk at a time, and
|
|
201
|
+
* end it. For a generated download — a CSV export, an NDJSON dump, a receipt —
|
|
202
|
+
* where the source is a generator over a cursor rather than a `Readable` that
|
|
203
|
+
* could simply be piped.
|
|
204
|
+
*
|
|
205
|
+
* The obvious loop is wrong in three ways that testing does not surface.
|
|
206
|
+
* `res.write()` returning `false` is easy to discard, and a local client
|
|
207
|
+
* drains instantly, so a bounded-memory export becomes unbounded only under a
|
|
208
|
+
* slow client. Always awaiting `'drain'` then hangs forever when the peer has
|
|
209
|
+
* gone, because a closed socket never emits it. And a producer that throws
|
|
210
|
+
* after the first byte cannot be turned into an error page: the status line is
|
|
211
|
+
* already sent, so the handler appends its message to the partial body and the
|
|
212
|
+
* client receives a 200 whose last row reads "Internal Server Error" — a
|
|
213
|
+
* truncated export that every consumer reads as complete.
|
|
214
|
+
*
|
|
215
|
+
* So: back-pressure is awaited, a closed peer stops the loop instead of
|
|
216
|
+
* stalling it, and a mid-stream throw destroys the connection. Destroying ends
|
|
217
|
+
* a chunked response without its terminating chunk, which is the only signal
|
|
218
|
+
* left that says "this transfer is incomplete" once bytes are on the wire. The
|
|
219
|
+
* error is re-thrown either way, so the caller still logs it.
|
|
220
|
+
*
|
|
221
|
+
* A producer that fails BEFORE yielding anything is not a truncated export —
|
|
222
|
+
* nothing was produced. Opening an async generator runs none of its body, so
|
|
223
|
+
* the first value is fetched while the status line is still unsent: a query
|
|
224
|
+
* that fails to run reaches the caller with a response it can still render an
|
|
225
|
+
* error page on, rather than as a download that dies partway. One row is
|
|
226
|
+
* therefore fetched before the headers go out, which is what any implementation
|
|
227
|
+
* must do to know whether the producer can produce at all.
|
|
228
|
+
*
|
|
229
|
+
* That wait belongs to a response THIS call commits. A long-lived stream whose
|
|
230
|
+
* first value is minutes away — an event subscription, a tail — should send its
|
|
231
|
+
* own head first (`res.writeHead(...)`, `res.flushHeaders()`) and then stream
|
|
232
|
+
* into it: with the status line already out there is nothing to hold back, so
|
|
233
|
+
* no value is fetched before the headers and the client sees the connection
|
|
234
|
+
* establish at once.
|
|
235
|
+
*
|
|
236
|
+
* Headers are written from `opts` before the first chunk unless the caller has
|
|
237
|
+
* already sent them. The `application/octet-stream` default steps aside for a
|
|
238
|
+
* Content-Type the route already set with `res.setHeader` — unlike the other
|
|
239
|
+
* helpers this one does not encode the body, so that default is a placeholder
|
|
240
|
+
* for an answer the caller has, and a route serving a CSV had already given it.
|
|
241
|
+
* An explicit `opts.headers` entry still wins over both, and the Cache-Control
|
|
242
|
+
* default does not step aside for anything but that. Nothing is buffered: a
|
|
243
|
+
* chunk is handed to the socket as the producer yields it.
|
|
244
|
+
*
|
|
245
|
+
* Pass a function instead of an iterable to be handed a signal that aborts when
|
|
246
|
+
* the client goes away, the caller aborts, or the stream fails. Cancelling a
|
|
247
|
+
* generator does not reach work it is already waiting on — a generator parked
|
|
248
|
+
* in `await query()` runs its `finally` only once that query returns on its
|
|
249
|
+
* own — so a producer holding a cursor, a connection or a file handle should
|
|
250
|
+
* take the signal and cancel with it.
|
|
251
|
+
*
|
|
252
|
+
* @opts
|
|
253
|
+
* status: 200, // numeric HTTP status, sent with the first chunk
|
|
254
|
+
* headers: {}, // merged over the dynamic-response defaults
|
|
255
|
+
* onError: "destroy", // "destroy" (default) or "rethrow" to leave the socket alone
|
|
256
|
+
* signal: AbortSignal, // stop producing when it aborts
|
|
257
|
+
*
|
|
258
|
+
* @example
|
|
259
|
+
* await b.render.stream(res, rows(), {
|
|
260
|
+
* headers: { "Content-Type": "text/csv; charset=utf-8" },
|
|
261
|
+
* });
|
|
262
|
+
*
|
|
263
|
+
* async function* rows() {
|
|
264
|
+
* yield "order_id,total\n";
|
|
265
|
+
* for await (var r of cursor) yield r.id + "," + r.total + "\n";
|
|
266
|
+
* }
|
|
267
|
+
*
|
|
268
|
+
* // Taking the signal lets the query itself be cancelled when the client
|
|
269
|
+
* // hangs up, instead of running to completion against a closed socket.
|
|
270
|
+
* await b.render.stream(res, function (signal) { return rows(signal); }, {
|
|
271
|
+
* headers: { "Content-Type": "text/csv; charset=utf-8" },
|
|
272
|
+
* });
|
|
273
|
+
*/
|
|
274
|
+
async function stream(res, iterable, opts) {
|
|
275
|
+
opts = opts || {};
|
|
276
|
+
validateOpts(opts, ["status", "headers", "onError", "signal"], "render.stream");
|
|
277
|
+
if (opts.onError !== undefined && opts.onError !== "destroy" && opts.onError !== "rethrow") {
|
|
278
|
+
throw new TypeError("render.stream: opts.onError must be \"destroy\" or \"rethrow\"");
|
|
279
|
+
}
|
|
280
|
+
if (_alreadyDone(res)) return;
|
|
281
|
+
|
|
282
|
+
// Read the caller's headers FIRST, while there is nothing to release. Copying
|
|
283
|
+
// them runs whatever `opts.headers` chooses to run — a getter, a proxy trap —
|
|
284
|
+
// and doing it after the producer was opened left a throw there with no path
|
|
285
|
+
// back to the cleanup, holding the producer's cursor or file handle for the
|
|
286
|
+
// life of the process. Here it is simply a bad argument, and reaches the
|
|
287
|
+
// caller as one.
|
|
288
|
+
var headers = _mergedHeaders(_defaultContentTypeUnlessStated(res, {
|
|
289
|
+
"Content-Type": "application/octet-stream",
|
|
290
|
+
"Cache-Control": DEFAULT_DYNAMIC_CACHE_CONTROL,
|
|
291
|
+
}), opts.headers);
|
|
292
|
+
|
|
293
|
+
// Stopping has to reach the producer, not only the loop. An async generator
|
|
294
|
+
// suspended inside its own `await` does not run its `finally` when `return()`
|
|
295
|
+
// is called: that call queues behind the very pull it is trying to cancel, so
|
|
296
|
+
// a cursor stays open until a query nobody is waiting for finishes on its
|
|
297
|
+
// own. Handing the producer a signal is the only way to reach work already in
|
|
298
|
+
// flight, so a producer that takes one is given this one.
|
|
299
|
+
var stopper = new AbortController();
|
|
300
|
+
var unlink = _linkSignal(opts.signal, stopper);
|
|
301
|
+
|
|
302
|
+
// The source is opened before anything is committed. A factory that throws,
|
|
303
|
+
// or an object whose `Symbol.asyncIterator` throws, then reaches the caller's
|
|
304
|
+
// error page normally — with the status line still unsent, rather than as a
|
|
305
|
+
// committed response with no body to follow.
|
|
306
|
+
// "Before the status line goes out" is this primitive's own status line. The
|
|
307
|
+
// CALLER may have sent one already — the documented shape where a route
|
|
308
|
+
// writes its own head and streams into it, which is how SSE is written — and
|
|
309
|
+
// then there is no pre-commit window to fail into: those headers are on the
|
|
310
|
+
// wire and the only honest ending left is an incomplete transfer. Every throw
|
|
311
|
+
// out of the opening sequence goes through here so that a response someone
|
|
312
|
+
// else committed is never left neither ended nor destroyed, which reads to
|
|
313
|
+
// the client as a body that never arrives.
|
|
314
|
+
function failBeforeStreaming(e, openedSource) {
|
|
315
|
+
if (openedSource) _stopProducer(stopper, openedSource, unlink);
|
|
316
|
+
else unlink();
|
|
317
|
+
if (res.headersSent === true && opts.onError !== "rethrow") {
|
|
318
|
+
requestHelpers().failAfterHeaders(res);
|
|
319
|
+
}
|
|
320
|
+
throw e;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
var source;
|
|
324
|
+
try {
|
|
325
|
+
source = _openSource(iterable, stopper.signal);
|
|
326
|
+
} catch (e) {
|
|
327
|
+
failBeforeStreaming(e, null);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Opening an async generator does no work — its body does not run until the
|
|
331
|
+
// first pull — so "the source opened" said nothing about whether the producer
|
|
332
|
+
// can produce, which is the source type this primitive is built around. The
|
|
333
|
+
// first value is fetched HERE, while the status line is still unsent, so a
|
|
334
|
+
// query that fails before any row reaches the caller's error page instead of
|
|
335
|
+
// arriving as a download that dies partway. That is the failure most likely
|
|
336
|
+
// to be seen in production, and the only one an error page can explain.
|
|
337
|
+
//
|
|
338
|
+
// A stop that arrives during the first pull is NOT a pre-commit failure: an
|
|
339
|
+
// aborted export is a truncation whether or not a row was ever produced, and
|
|
340
|
+
// it takes the committed path below so the client hears an incomplete
|
|
341
|
+
// transfer rather than a successful empty one.
|
|
342
|
+
// Only where THIS call is the one that will commit. A route that sent its own
|
|
343
|
+
// status line has no pre-commit window to protect, and waiting for a first
|
|
344
|
+
// value there buys nothing while costing everything: an event stream whose
|
|
345
|
+
// first event is minutes away would hold its headers back for minutes, and
|
|
346
|
+
// the client would see a connection that never established. That shape — send
|
|
347
|
+
// the head, flush it, then stream — is also the escape hatch for a long-lived
|
|
348
|
+
// producer that wants its headers out before it has anything to say.
|
|
349
|
+
var closed = _closedSignal(res);
|
|
350
|
+
var stopped = null; // "abort" | "peer" | null
|
|
351
|
+
var pending = null; // the first step, held back
|
|
352
|
+
var willCommitHere = res.headersSent !== true && typeof res.writeHead === "function";
|
|
353
|
+
if (!willCommitHere) pending = null;
|
|
354
|
+
else if (stopper.signal.aborted) stopped = "abort";
|
|
355
|
+
else if (_peerGone(res)) stopped = "peer";
|
|
356
|
+
else {
|
|
357
|
+
try {
|
|
358
|
+
// Until the producer offers something that would actually be WRITTEN, or
|
|
359
|
+
// says it is done. A `null` or `undefined` is skipped by the loop below
|
|
360
|
+
// rather than written, so it is not proof the producer can produce — a
|
|
361
|
+
// source that yields one and then fails would otherwise have committed
|
|
362
|
+
// the response on the strength of a value nobody ever sends.
|
|
363
|
+
for (;;) {
|
|
364
|
+
pending = _requireIteratorResult(
|
|
365
|
+
await _raceStop(Promise.resolve(source.next()), stopper.signal, closed));
|
|
366
|
+
if (pending.done) break;
|
|
367
|
+
// A SYNC iterable may yield a PROMISE, and `for await` resolves it
|
|
368
|
+
// before handing it on — so a rejected one is another failure before
|
|
369
|
+
// any chunk was produced, and it belongs in this window rather than in
|
|
370
|
+
// the loop. Resolved here, the loop sees a settled value and writes it.
|
|
371
|
+
if (!source.isAsync && pending.value && typeof pending.value.then === "function") {
|
|
372
|
+
pending = {
|
|
373
|
+
done: false,
|
|
374
|
+
value: await _raceStop(pending.value, stopper.signal, closed),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
if (pending.value !== null && pending.value !== undefined) break;
|
|
378
|
+
}
|
|
379
|
+
} catch (e) {
|
|
380
|
+
if (e && e.stopKind) stopped = e.stopKind;
|
|
381
|
+
else {
|
|
382
|
+
closed.dispose();
|
|
383
|
+
failBeforeStreaming(e, source);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (res.headersSent !== true && typeof res.writeHead === "function") {
|
|
389
|
+
try {
|
|
390
|
+
res.writeHead(_statusOr(opts, C.HTTP.STATUS.OK), headers);
|
|
391
|
+
} catch (e) {
|
|
392
|
+
// A status out of range or a header value Node refuses. The producer is
|
|
393
|
+
// already open by now, so a configuration mistake would otherwise leave
|
|
394
|
+
// its cursor or file handle held for the life of the process. A partial
|
|
395
|
+
// writeHead can also leave the response committed, which is why this
|
|
396
|
+
// takes the same route out as every other failure before the loop.
|
|
397
|
+
closed.dispose();
|
|
398
|
+
failBeforeStreaming(e, source);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// The status line went out above, so from here the response is committed
|
|
403
|
+
// whether or not a chunk has been written yet. Treating "committed" as "at
|
|
404
|
+
// least one chunk landed" left a producer that failed on its first row with
|
|
405
|
+
// a response that was neither ended nor destroyed — headers and
|
|
406
|
+
// `Transfer-Encoding: chunked` on the wire and nothing to follow, so the
|
|
407
|
+
// client waited forever.
|
|
408
|
+
// Three things can stop this loop and each has to release the producer: the
|
|
409
|
+
// producer failing, the caller aborting, and the peer going away. A
|
|
410
|
+
// `for await` releases the iterator only when IT decides the loop is over,
|
|
411
|
+
// so a pull that never settles — a query that hangs, a page that never
|
|
412
|
+
// arrives — kept the handler, the cursor and the response alive with no way
|
|
413
|
+
// out. The loop owns its own stopping instead.
|
|
414
|
+
try {
|
|
415
|
+
for (;;) {
|
|
416
|
+
if (stopped !== null) break; // decided before the commit
|
|
417
|
+
if (stopper.signal.aborted) { stopped = "abort"; break; }
|
|
418
|
+
// The peer is gone: stop pulling rows nobody will read, and do not wait
|
|
419
|
+
// for a drain that never comes.
|
|
420
|
+
if (_peerGone(res)) { stopped = "peer"; break; }
|
|
421
|
+
var step;
|
|
422
|
+
if (pending !== null) { step = pending; pending = null; } // the pre-commit pull
|
|
423
|
+
else {
|
|
424
|
+
try {
|
|
425
|
+
step = _requireIteratorResult(
|
|
426
|
+
await _raceStop(Promise.resolve(source.next()), stopper.signal, closed));
|
|
427
|
+
} catch (e) {
|
|
428
|
+
if (e && e.stopKind) { stopped = e.stopKind; break; }
|
|
429
|
+
throw e;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (step.done) break;
|
|
433
|
+
var chunk = step.value;
|
|
434
|
+
try {
|
|
435
|
+
// A synchronous iterable may still yield promises, and `for await`
|
|
436
|
+
// resolves each one before handing it on. Writing the promise object
|
|
437
|
+
// instead would put "[object Promise]" in the export.
|
|
438
|
+
if (!source.isAsync && chunk && typeof chunk.then === "function") {
|
|
439
|
+
chunk = await _raceStop(chunk, stopper.signal, closed);
|
|
440
|
+
}
|
|
441
|
+
if (chunk === null || chunk === undefined) continue;
|
|
442
|
+
await _raceStop(safeAsync().writeChunk(res, chunk), stopper.signal, closed);
|
|
443
|
+
} catch (e2) {
|
|
444
|
+
if (e2 && e2.stopKind) { stopped = e2.stopKind; break; }
|
|
445
|
+
throw e2;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
} catch (e) {
|
|
449
|
+
_stopProducer(stopper, source, unlink);
|
|
450
|
+
closed.dispose();
|
|
451
|
+
if (opts.onError !== "rethrow") requestHelpers().failAfterHeaders(res);
|
|
452
|
+
throw e;
|
|
453
|
+
}
|
|
454
|
+
// Only stop a producer the loop LEFT early. A `for await` calls `return()` on
|
|
455
|
+
// an early exit and not on normal exhaustion, and producers use it to record
|
|
456
|
+
// exactly that — telling one its completed export was cancelled is a
|
|
457
|
+
// different statement.
|
|
458
|
+
if (stopped !== null) _stopProducer(stopper, source, unlink);
|
|
459
|
+
else unlink();
|
|
460
|
+
closed.dispose();
|
|
461
|
+
// An abort is a truncation too. Ending normally would write the terminating
|
|
462
|
+
// chunk and hand the client four rows of a fifty-row export as a complete,
|
|
463
|
+
// successful 200 — the very outcome this primitive exists to prevent, and
|
|
464
|
+
// the caller would not hear about it either. A signal that was already
|
|
465
|
+
// aborted when the call began takes the same path: an empty export is still
|
|
466
|
+
// a truncated one.
|
|
467
|
+
if (stopped !== null) {
|
|
468
|
+
requestHelpers().failAfterHeaders(res);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (typeof res.end === "function" && !_alreadyDone(res)) res.end();
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Open the producer. A function is called with the stop signal, so a producer
|
|
475
|
+
// whose work is cancellable can be handed the one thing that reaches work
|
|
476
|
+
// already in flight; anything else is used as the iterable it claims to be.
|
|
477
|
+
function _openSource(iterable, signal) {
|
|
478
|
+
var it = typeof iterable === "function" ? iterable(signal) : iterable;
|
|
479
|
+
var isAsync = !!(it && typeof it[Symbol.asyncIterator] === "function");
|
|
480
|
+
if (!it || (!isAsync && typeof it[Symbol.iterator] !== "function")) {
|
|
481
|
+
throw new TypeError("render.stream: expected an async or sync iterable of chunks, " +
|
|
482
|
+
"or a function returning one");
|
|
483
|
+
}
|
|
484
|
+
var iterator = isAsync ? it[Symbol.asyncIterator]() : it[Symbol.iterator]();
|
|
485
|
+
// What the method HANDED BACK has to be an iterator, and that is asked here
|
|
486
|
+
// rather than at the first pull. Left until then, a method returning `null`,
|
|
487
|
+
// a number, or an object with no `next` came to light after the status line
|
|
488
|
+
// had gone out — so a malformed producer arrived as a committed response that
|
|
489
|
+
// was then destroyed, instead of as a bad argument the caller could still
|
|
490
|
+
// render an error page for.
|
|
491
|
+
if (!iterator || (typeof iterator !== "object" && typeof iterator !== "function") ||
|
|
492
|
+
typeof iterator.next !== "function") {
|
|
493
|
+
throw new TypeError("render.stream: the iterable's " +
|
|
494
|
+
(isAsync ? "Symbol.asyncIterator" : "Symbol.iterator") +
|
|
495
|
+
" returned something that is not an iterator — it must return an object " +
|
|
496
|
+
"with a next() method");
|
|
497
|
+
}
|
|
498
|
+
return {
|
|
499
|
+
isAsync: isAsync,
|
|
500
|
+
next: function () { return iterator.next(); },
|
|
501
|
+
"return": function () {
|
|
502
|
+
return typeof iterator["return"] === "function" ? iterator["return"]() : undefined;
|
|
503
|
+
},
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// The iterator protocol says a step is an object. Treating anything else as "no
|
|
508
|
+
// chunk this time" would pull again from a producer that can only answer the
|
|
509
|
+
// same way, and spin forever with the response held open; `for await` raises
|
|
510
|
+
// here and so does this. Asked of the FIRST step too, which is before the status
|
|
511
|
+
// line goes out, so a producer that answers wrongly from the start is a bad
|
|
512
|
+
// argument rather than a committed response.
|
|
513
|
+
function _requireIteratorResult(step) {
|
|
514
|
+
if (!step || (typeof step !== "object" && typeof step !== "function")) {
|
|
515
|
+
throw new TypeError("render.stream: the producer returned " +
|
|
516
|
+
(step === undefined ? "undefined" : JSON.stringify(step)) +
|
|
517
|
+
" where an iterator result was expected");
|
|
518
|
+
}
|
|
519
|
+
return step;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Stop the producer, signal first. `return()` alone is not enough: on an async
|
|
523
|
+
// generator it queues behind the pull it means to cancel, so a generator parked
|
|
524
|
+
// in `await query()` does not reach its `finally` — and the cursor, file handle
|
|
525
|
+
// or connection it holds stays open — until that query finishes on its own. The
|
|
526
|
+
// signal reaches the pending work; `return()` then unwinds the generator.
|
|
527
|
+
//
|
|
528
|
+
// Neither is awaited. A producer blocked on the same thing that stalled the
|
|
529
|
+
// pull would otherwise block the response's own teardown behind it.
|
|
530
|
+
function _stopProducer(stopper, source, unlink) {
|
|
531
|
+
unlink();
|
|
532
|
+
try { stopper.abort(); } catch (_a) { /* already aborted */ }
|
|
533
|
+
if (!source) return;
|
|
534
|
+
try {
|
|
535
|
+
var maybe = source["return"]();
|
|
536
|
+
if (maybe && typeof maybe.then === "function") maybe.then(_ignore, _ignore);
|
|
537
|
+
} catch (_e) { /* the producer is entitled to refuse */ }
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Forward the caller's abort to the stop signal, and hand back the undo. A
|
|
541
|
+
// per-request signal outlives a single export, so the listener is removed when
|
|
542
|
+
// the stream is over rather than left to accumulate one per response.
|
|
543
|
+
function _linkSignal(signal, stopper) {
|
|
544
|
+
if (!signal) return _ignore;
|
|
545
|
+
if (signal.aborted) {
|
|
546
|
+
stopper.abort();
|
|
547
|
+
return _ignore;
|
|
548
|
+
}
|
|
549
|
+
function onAbort() { stopper.abort(); }
|
|
550
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
551
|
+
return function () { signal.removeEventListener("abort", onAbort); };
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function _ignore() {}
|
|
555
|
+
|
|
556
|
+
// Take delivery of a promise whose outcome no longer matters, so that a
|
|
557
|
+
// rejection nobody is waiting for does not surface as an unhandled one.
|
|
558
|
+
function _observe(promise) {
|
|
559
|
+
if (promise && typeof promise.then === "function") promise.then(_ignore, _ignore);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// A promise that settles when the response closes, so a pull blocked on the
|
|
563
|
+
// next database page does not outlive the client that asked for it. The
|
|
564
|
+
// listener is removed either way — a long-lived response would otherwise
|
|
565
|
+
// accumulate one per stream.
|
|
566
|
+
function _closedSignal(res) {
|
|
567
|
+
var fired = _peerGone(res);
|
|
568
|
+
// A subscriber list rather than a promise. Racing a promise would attach one
|
|
569
|
+
// reaction per chunk to the same unresolved promise, and a settled race
|
|
570
|
+
// cannot detach it — so a long export accumulated a closure for every row it
|
|
571
|
+
// wrote, and the memory it was careful not to spend on buffering went here
|
|
572
|
+
// instead.
|
|
573
|
+
var waiting = [];
|
|
574
|
+
function onClose() {
|
|
575
|
+
fired = true;
|
|
576
|
+
var pending = waiting;
|
|
577
|
+
waiting = [];
|
|
578
|
+
for (var i = 0; i < pending.length; i += 1) pending[i]();
|
|
579
|
+
}
|
|
580
|
+
if (res && typeof res.once === "function") res.once("close", onClose);
|
|
581
|
+
return {
|
|
582
|
+
isClosed: function () { return fired || _peerGone(res); },
|
|
583
|
+
subscribe: function (fn) {
|
|
584
|
+
if (fired) { fn(); return function () {}; }
|
|
585
|
+
waiting.push(fn);
|
|
586
|
+
return function () {
|
|
587
|
+
var at = waiting.indexOf(fn);
|
|
588
|
+
if (at !== -1) waiting.splice(at, 1);
|
|
589
|
+
};
|
|
590
|
+
},
|
|
591
|
+
dispose: function () {
|
|
592
|
+
waiting = [];
|
|
593
|
+
if (res && typeof res.removeListener === "function") res.removeListener("close", onClose);
|
|
594
|
+
},
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Settle as soon as the caller aborts or the peer closes, so neither a blocked
|
|
599
|
+
// producer nor a wait for `drain` from a peer that stopped reading outlives the
|
|
600
|
+
// reason to keep going.
|
|
601
|
+
function _raceStop(promise, signal, closed) {
|
|
602
|
+
if (!signal && !closed) return promise;
|
|
603
|
+
// Already stopped before the race could be set up — but the pull is in flight
|
|
604
|
+
// whatever we decide about it. Left unobserved, its later rejection is an
|
|
605
|
+
// unhandled rejection, which by default takes the process down: a producer
|
|
606
|
+
// that cancels itself and then lets its query reject would kill the server.
|
|
607
|
+
if (signal && signal.aborted) {
|
|
608
|
+
_observe(promise);
|
|
609
|
+
return Promise.reject(_stopError("abort"));
|
|
610
|
+
}
|
|
611
|
+
if (closed && closed.isClosed()) {
|
|
612
|
+
_observe(promise);
|
|
613
|
+
return Promise.reject(_stopError("peer"));
|
|
614
|
+
}
|
|
615
|
+
return new Promise(function (resolve, reject) {
|
|
616
|
+
var settled = false;
|
|
617
|
+
var unsubscribe = null;
|
|
618
|
+
function done(fn, v) {
|
|
619
|
+
if (settled) return;
|
|
620
|
+
settled = true;
|
|
621
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
622
|
+
if (unsubscribe) unsubscribe();
|
|
623
|
+
fn(v);
|
|
624
|
+
}
|
|
625
|
+
function onAbort() { done(reject, _stopError("abort")); }
|
|
626
|
+
if (signal) signal.addEventListener("abort", onAbort, { once: true });
|
|
627
|
+
if (closed) unsubscribe = closed.subscribe(function () { done(reject, _stopError("peer")); });
|
|
628
|
+
promise.then(function (v) { done(resolve, v); }, function (e) { done(reject, e); });
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function _stopError(kind) {
|
|
633
|
+
var err = new Error("render.stream: " + (kind === "abort" ? "aborted" : "the peer closed"));
|
|
634
|
+
err.code = kind === "abort" ? "render/aborted" : "render/peer-closed";
|
|
635
|
+
err.stopKind = kind;
|
|
636
|
+
return err;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// A response whose socket has been destroyed or whose peer has hung up. Both
|
|
640
|
+
// are reported differently across Node versions and response doubles, so all
|
|
641
|
+
// three signals are consulted.
|
|
642
|
+
function _peerGone(res) {
|
|
643
|
+
if (!res) return true;
|
|
644
|
+
if (res.destroyed === true || res.writableEnded === true) return true;
|
|
645
|
+
return !!(res.socket && res.socket.destroyed === true);
|
|
124
646
|
}
|
|
125
647
|
|
|
126
648
|
/**
|
|
@@ -153,7 +675,7 @@ function text(res, body, opts) {
|
|
|
153
675
|
"Content-Length": Buffer.byteLength(encoded, charset),
|
|
154
676
|
"Cache-Control": DEFAULT_DYNAMIC_CACHE_CONTROL,
|
|
155
677
|
}, opts.headers);
|
|
156
|
-
_writeResponse(res, opts.
|
|
678
|
+
_writeResponse(res, _statusOr(opts, C.HTTP.STATUS.OK), headers, encoded);
|
|
157
679
|
}
|
|
158
680
|
|
|
159
681
|
/**
|
|
@@ -187,7 +709,7 @@ function htmlString(res, htmlBody, opts) {
|
|
|
187
709
|
"Content-Length": Buffer.byteLength(encoded, charset),
|
|
188
710
|
"Cache-Control": DEFAULT_DYNAMIC_CACHE_CONTROL,
|
|
189
711
|
}, opts.headers);
|
|
190
|
-
_writeResponse(res, opts.
|
|
712
|
+
_writeResponse(res, _statusOr(opts, C.HTTP.STATUS.OK), headers, encoded);
|
|
191
713
|
}
|
|
192
714
|
|
|
193
715
|
/**
|
|
@@ -217,8 +739,8 @@ function redirect(res, location, opts) {
|
|
|
217
739
|
if (typeof location !== "string" || location.length === 0) {
|
|
218
740
|
throw new Error("render.redirect: location is required");
|
|
219
741
|
}
|
|
220
|
-
var status = opts.
|
|
221
|
-
if (status
|
|
742
|
+
var status = _statusOr(opts, C.HTTP.STATUS.FOUND);
|
|
743
|
+
if (!C.HTTP.redirect(status)) {
|
|
222
744
|
throw new Error("render.redirect: status must be 3xx (got " + status + ")");
|
|
223
745
|
}
|
|
224
746
|
var headers = _mergedHeaders({
|
|
@@ -292,6 +814,7 @@ function create(opts) {
|
|
|
292
814
|
html: html,
|
|
293
815
|
htmlString: htmlString,
|
|
294
816
|
json: json,
|
|
817
|
+
stream: stream,
|
|
295
818
|
text: text,
|
|
296
819
|
redirect: redirect,
|
|
297
820
|
engine: engine,
|
|
@@ -301,6 +824,7 @@ function create(opts) {
|
|
|
301
824
|
module.exports = {
|
|
302
825
|
create: create,
|
|
303
826
|
json: json,
|
|
827
|
+
stream: stream,
|
|
304
828
|
text: text,
|
|
305
829
|
htmlString: htmlString,
|
|
306
830
|
redirect: redirect,
|