orionplace-react 0.3.0 → 0.3.1
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orionplace-react",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
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/src/project-readme.mjs
CHANGED
|
@@ -89,7 +89,7 @@ and Next.js configuration are preserved.
|
|
|
89
89
|
| \`../${base}app/_storefront/\` | Route controllers that connect components, navigation and API actions. |
|
|
90
90
|
| \`../${base}app/api/[...orionplace]/route.ts\` | The route handler serving \`/api/...\` from your project. |
|
|
91
91
|
| \`../${base}app/ps3/blog/[...orionplace]/route.ts\` | The route handler serving guarded blog images and videos under \`/ps3/blog/...\`. |
|
|
92
|
-
| \`../${base}proxy.ts\` | Marks personalized pages private and
|
|
92
|
+
| \`../${base}proxy.ts\` | Marks personalized pages private and answers HTTP 503 when Application Load fails. |
|
|
93
93
|
| \`../sitemaps/${frontend}.cfg\` | The backend URL used for this frontend domain. |
|
|
94
94
|
| \`../.env\` | Local MIMIC setting; keep secrets out of version control. |
|
|
95
95
|
|
|
@@ -311,9 +311,12 @@ Failures also survive hydration and navigation. They never open a login modal or
|
|
|
311
311
|
trigger an automatic retry on every route. The supplied shell shows
|
|
312
312
|
\`StorefrontErrorPage\` and keeps the header, route content and footer unmounted
|
|
313
313
|
until initialization succeeds. Retry reloads the page; an unknown address keeps
|
|
314
|
-
the 404 page. App Router
|
|
315
|
-
|
|
316
|
-
|
|
314
|
+
the 404 page. App Router pages cannot change their status once they stream, so
|
|
315
|
+
\`${base}proxy.ts\` starts Application Load for a full page load and hands it
|
|
316
|
+
to the render (still one request): when it fails, the error page is sent with
|
|
317
|
+
HTTP 503. A project that keeps its own \`proxy.ts\` sends it with HTTP 200 unless
|
|
318
|
+
it does the same. Feature components should check \`selectIsReady\` before using
|
|
319
|
+
required data.
|
|
317
320
|
|
|
318
321
|
Page responses use \`private, no-store\` through \`${base}proxy.ts\`. Do not put
|
|
319
322
|
personalized HTML in shared caches or persist the snapshot in localStorage. The
|
|
@@ -31,8 +31,25 @@ const FORWARDED = [
|
|
|
31
31
|
'x-tz',
|
|
32
32
|
];
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
/** Header the proxy sets on a page request whose Application Load it started. */
|
|
35
|
+
export const APP_LOAD_HANDOFF_HEADER = 'x-orionplace-appload';
|
|
36
|
+
/** How long a started Application Load waits for the layout to take it. */
|
|
37
|
+
const HANDOFF_TTL = 60_000;
|
|
38
|
+
const HANDOFF_KEY = Symbol.for('orionplace.appLoadHandoff');
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Started loads by id. Kept on `globalThis`: the proxy and the page render run
|
|
42
|
+
* in the same Node process but in separate bundles.
|
|
43
|
+
*/
|
|
44
|
+
function handoffs(): Map<string, Promise<ServerAppLoad>> {
|
|
45
|
+
const scope = globalThis as typeof globalThis & {
|
|
46
|
+
[HANDOFF_KEY]?: Map<string, Promise<ServerAppLoad>>;
|
|
47
|
+
};
|
|
48
|
+
scope[HANDOFF_KEY] ??= new Map();
|
|
49
|
+
return scope[HANDOFF_KEY];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function buildRequest(incoming: Headers, signal: AbortSignal): Request {
|
|
36
53
|
const host =
|
|
37
54
|
incoming.get('x-forwarded-host') || incoming.get('host') || 'localhost';
|
|
38
55
|
const protocol =
|
|
@@ -74,12 +91,14 @@ async function buildRequest(signal: AbortSignal): Promise<Request> {
|
|
|
74
91
|
*/
|
|
75
92
|
export async function requestAppLoad(
|
|
76
93
|
signal?: AbortSignal,
|
|
94
|
+
incoming?: Headers,
|
|
77
95
|
): Promise<ServerAppLoad> {
|
|
78
96
|
const timeout = AbortSignal.timeout(35_000);
|
|
79
97
|
const combined = signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
80
98
|
let response: Response;
|
|
81
99
|
try {
|
|
82
|
-
|
|
100
|
+
const request = buildRequest(incoming ?? new Headers(await headers()), combined);
|
|
101
|
+
response = await forwardMarketplace(request, {
|
|
83
102
|
mimicFrontDomain: process.env.MIMIC_FRONT_DOMAIN ?? '',
|
|
84
103
|
});
|
|
85
104
|
} catch {
|
|
@@ -107,11 +126,37 @@ export async function requestAppLoad(
|
|
|
107
126
|
};
|
|
108
127
|
}
|
|
109
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Starts Application Load for a page request in `proxy.ts`, before the page
|
|
131
|
+
* renders, so a failure can answer HTTP 503 (a page cannot change its status
|
|
132
|
+
* once it streams). Returns the id to pass in `APP_LOAD_HANDOFF_HEADER`; the
|
|
133
|
+
* render takes the same result, so the page still costs one request.
|
|
134
|
+
*/
|
|
135
|
+
export function startAppLoad(incoming: Headers): {
|
|
136
|
+
id: string;
|
|
137
|
+
load: Promise<ServerAppLoad>;
|
|
138
|
+
} {
|
|
139
|
+
const id = crypto.randomUUID();
|
|
140
|
+
const load = requestAppLoad(undefined, incoming);
|
|
141
|
+
const store = handoffs();
|
|
142
|
+
store.set(id, load);
|
|
143
|
+
setTimeout(() => store.delete(id), HANDOFF_TTL).unref?.();
|
|
144
|
+
return { id, load };
|
|
145
|
+
}
|
|
146
|
+
|
|
110
147
|
/**
|
|
111
148
|
* One request per server render, shared by the layout and any server component
|
|
112
149
|
* that awaits it. Reading it in a layout initializes the marketplace before the
|
|
113
|
-
* page renders; it never opens an authentication prompt.
|
|
150
|
+
* page renders; it never opens an authentication prompt. A load the proxy
|
|
151
|
+
* started for this request is taken once; otherwise the render requests it.
|
|
114
152
|
*/
|
|
115
|
-
export const getOrionplaceAppLoad = cache((): Promise<ServerAppLoad> =>
|
|
116
|
-
|
|
117
|
-
);
|
|
153
|
+
export const getOrionplaceAppLoad = cache(async (): Promise<ServerAppLoad> => {
|
|
154
|
+
const incoming = await headers();
|
|
155
|
+
const id = incoming.get(APP_LOAD_HANDOFF_HEADER);
|
|
156
|
+
const started = id ? handoffs().get(id) : undefined;
|
|
157
|
+
if (id && started) {
|
|
158
|
+
handoffs().delete(id);
|
|
159
|
+
return started;
|
|
160
|
+
}
|
|
161
|
+
return requestAppLoad(undefined, new Headers(incoming));
|
|
162
|
+
});
|
package/templates/proxy.ts
CHANGED
|
@@ -1,17 +1,51 @@
|
|
|
1
1
|
import { NextResponse, type NextRequest } from 'next/server';
|
|
2
|
+
import {
|
|
3
|
+
APP_LOAD_HANDOFF_HEADER,
|
|
4
|
+
startAppLoad,
|
|
5
|
+
} from '@/lib/orionplace/appLoadServer';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A full page load: not an in-app navigation (`RSC`), a prefetch or a public
|
|
9
|
+
* file such as an image.
|
|
10
|
+
*/
|
|
11
|
+
function pageLoad(request: NextRequest) {
|
|
12
|
+
const accept = request.headers.get('accept') ?? '';
|
|
13
|
+
return (
|
|
14
|
+
request.method === 'GET' &&
|
|
15
|
+
!request.headers.has('rsc') &&
|
|
16
|
+
!request.headers.has('next-router-prefetch') &&
|
|
17
|
+
!/\.[a-z0-9]+$/i.test(request.nextUrl.pathname) &&
|
|
18
|
+
(!accept || accept.includes('text/html') || accept.includes('*/*'))
|
|
19
|
+
);
|
|
20
|
+
}
|
|
2
21
|
|
|
3
22
|
/**
|
|
4
23
|
* Personalized pages are never shared between visitors or storefront domains.
|
|
5
24
|
* Next.js manages its own `Vary` header for pages; the API gateway sets the
|
|
6
25
|
* storefront-domain `Vary` on its own responses.
|
|
26
|
+
*
|
|
27
|
+
* A full page load starts Application Load here and hands it to the render:
|
|
28
|
+
* when it fails, the error page answers HTTP 503 like the Nuxt and SvelteKit
|
|
29
|
+
* starters (an unknown address keeps its 404).
|
|
7
30
|
*/
|
|
8
|
-
export function proxy(request: NextRequest) {
|
|
31
|
+
export async function proxy(request: NextRequest) {
|
|
9
32
|
const forwarded = new Headers(request.headers);
|
|
33
|
+
// Only this proxy names a started load; a visitor cannot pick one.
|
|
34
|
+
forwarded.delete(APP_LOAD_HANDOFF_HEADER);
|
|
10
35
|
forwarded.set(
|
|
11
36
|
'x-orionplace-page-url',
|
|
12
37
|
request.nextUrl.pathname + request.nextUrl.search,
|
|
13
38
|
);
|
|
14
|
-
|
|
39
|
+
let unavailable = false;
|
|
40
|
+
if (pageLoad(request)) {
|
|
41
|
+
const { id, load } = startAppLoad(forwarded);
|
|
42
|
+
forwarded.set(APP_LOAD_HANDOFF_HEADER, id);
|
|
43
|
+
unavailable = Boolean((await load).failure);
|
|
44
|
+
}
|
|
45
|
+
const response = NextResponse.next({
|
|
46
|
+
request: { headers: forwarded },
|
|
47
|
+
...(unavailable ? { status: 503 } : {}),
|
|
48
|
+
});
|
|
15
49
|
response.headers.set('Cache-Control', 'private, no-store');
|
|
16
50
|
return response;
|
|
17
51
|
}
|