orionplace-react 0.7.5 → 0.7.6
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 +3 -1
- package/package.json +1 -1
- package/runtime/marketplace.mjs +83 -5
package/README.md
CHANGED
|
@@ -480,7 +480,9 @@ authorization and cookies are forwarded. Backend statuses are retained and cooki
|
|
|
480
480
|
Domain attributes are removed to bind cookies to the storefront. Gateway responses
|
|
481
481
|
use `Cache-Control: no-store`, except public blog images and videos: they keep the
|
|
482
482
|
backend's `private, max-age` rule, so the browser does not download them again on
|
|
483
|
-
every page.
|
|
483
|
+
every page. When the network drops the connection before the backend answers, GET,
|
|
484
|
+
HEAD and Application Load requests are sent once more on a new connection; other
|
|
485
|
+
requests are never repeated. Network failures return `502`.
|
|
484
486
|
|
|
485
487
|
A changing request (POST, PUT, PATCH, DELETE) from another site is rejected with
|
|
486
488
|
`403`. A request from the page's own host or the storefront domain passes, and the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orionplace-react",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.6",
|
|
4
4
|
"description": "Next.js 16 storefront for your Orionplace marketplace: setup CLI, server API gateway, typed store and ready-made catalog, cart, checkout and account components in English and Russian, plus an Android app build.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/runtime/marketplace.mjs
CHANGED
|
@@ -142,6 +142,74 @@ export function marketplaceConfigDirectory(root = process.cwd()) {
|
|
|
142
142
|
: resolve(root, 'sitemaps');
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
// The network can drop a pooled connection to the backend (a reset somewhere
|
|
146
|
+
// between this server and the API host). The request then fails before any
|
|
147
|
+
// answer arrives, so a request that is safe to repeat gets one more try on a
|
|
148
|
+
// new connection instead of a failed page.
|
|
149
|
+
const droppedConnection = new Set([
|
|
150
|
+
'ECONNRESET',
|
|
151
|
+
'ECONNABORTED',
|
|
152
|
+
'EPIPE',
|
|
153
|
+
'UND_ERR_SOCKET',
|
|
154
|
+
'UND_ERR_CLOSED',
|
|
155
|
+
]);
|
|
156
|
+
// Application Load only reads, and its body is a small JSON object; a larger
|
|
157
|
+
// body is streamed once without a retry.
|
|
158
|
+
const appLoadPath = /^\/api\/appload\/?$/;
|
|
159
|
+
const repeatBodyLimit = 64 * 1024;
|
|
160
|
+
|
|
161
|
+
/** @param {any} error */
|
|
162
|
+
function connectionDropped(error) {
|
|
163
|
+
return droppedConnection.has(error?.cause?.code ?? error?.code);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The upstream body and whether the request may be sent twice: GET and HEAD
|
|
168
|
+
* have no body; Application Load keeps its small body for a second try.
|
|
169
|
+
* @param {Request} request @param {string} pathname
|
|
170
|
+
* @returns {Promise<{body: any, repeatable: boolean}>}
|
|
171
|
+
*/
|
|
172
|
+
async function upstreamBody(request, pathname) {
|
|
173
|
+
if (['GET', 'HEAD'].includes(request.method)) {
|
|
174
|
+
return { body: undefined, repeatable: true };
|
|
175
|
+
}
|
|
176
|
+
if (!request.body || !appLoadPath.test(pathname)) {
|
|
177
|
+
return { body: request.body, repeatable: false };
|
|
178
|
+
}
|
|
179
|
+
const reader = request.body.getReader();
|
|
180
|
+
const chunks = [];
|
|
181
|
+
let size = 0;
|
|
182
|
+
for (;;) {
|
|
183
|
+
const { done, value } = await reader.read();
|
|
184
|
+
if (done) {
|
|
185
|
+
return { body: Buffer.concat(chunks), repeatable: true };
|
|
186
|
+
}
|
|
187
|
+
chunks.push(value);
|
|
188
|
+
size += value.byteLength;
|
|
189
|
+
if (size > repeatBodyLimit) {
|
|
190
|
+
const body = new ReadableStream({
|
|
191
|
+
start(controller) {
|
|
192
|
+
for (const chunk of chunks) {
|
|
193
|
+
controller.enqueue(chunk);
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
async pull(controller) {
|
|
197
|
+
const next = await reader.read();
|
|
198
|
+
if (next.done) {
|
|
199
|
+
controller.close();
|
|
200
|
+
} else {
|
|
201
|
+
controller.enqueue(next.value);
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
cancel(reason) {
|
|
205
|
+
return reader.cancel(reason);
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
return { body, repeatable: false };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
145
213
|
/** @param {number} status @param {string} message */
|
|
146
214
|
function failure(status, message) {
|
|
147
215
|
return Response.json(
|
|
@@ -271,13 +339,23 @@ export async function forwardMarketplace(request, configuration = {}) {
|
|
|
271
339
|
]),
|
|
272
340
|
duplex: 'half',
|
|
273
341
|
};
|
|
274
|
-
if (!['GET', 'HEAD'].includes(request.method)) {
|
|
275
|
-
options.body = request.body;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
342
|
let upstream;
|
|
279
343
|
try {
|
|
280
|
-
|
|
344
|
+
const { body, repeatable } = await upstreamBody(request, incoming.pathname);
|
|
345
|
+
if (body !== undefined) {
|
|
346
|
+
options.body = body;
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
upstream = await fetch(target, options);
|
|
350
|
+
} catch (error) {
|
|
351
|
+
if (!repeatable || !connectionDropped(error) || options.signal.aborted) {
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
354
|
+
console.warn(
|
|
355
|
+
`[orionplace] Backend connection dropped (${error?.cause?.code ?? error?.code}), retrying once`,
|
|
356
|
+
);
|
|
357
|
+
upstream = await fetch(target, options);
|
|
358
|
+
}
|
|
281
359
|
} catch (error) {
|
|
282
360
|
// The browser receives only the status; keep the cause in the server log.
|
|
283
361
|
if (!request.signal.aborted) {
|