Application initialization
Application Load · React
Application Load is the first request your storefront makes to its personal backend. It returns the settings and initial content needed to render the interface, the current customer profile when signed in, and the API methods available to this session.
/api/apploadWhat the response contains
page_data — the application data inside the API response. Its four main sections have different roles:
Initial storefront data, customer profile and available API endpoints.
interfaces/orionplace/appBase/IAppLoad.tsimport type { IAppLoadCommon } from '@/interfaces/orionplace/appBase/IAppLoadCommon';
import type { IApploadUserProfile } from '@/interfaces/orionplace/appBase/IApploadUserProfile';
import type { IAppLoadPathsRecord } from '@/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonPath';
export interface IAppLoad {
// Shared storefront settings and initial data.
init: IAppLoadCommon;
// Public API methods, grouped under paths.
unauth: IAppLoadPathsRecord;
// Current customer profile. Omitted for guests.
user?: IApploadUserProfile;
// Authenticated-session methods. Omitted for guests; the backend checks
// permissions.
auth?: IAppLoadPathsRecord;
// Whether seller-panel features are enabled for this storefront.
seller_panel_enabled?: boolean;
// Product-preview key for a signed-in account with seller information.
// Do not publish its value; buyer-facing interfaces do not need this key.
view_key?: string;
// Backend Application Load preparation time in seconds; only with project
// debug enabled.
// Excludes response serialization and network transfer.
spent_total?: number;
// Backend init loading time in seconds. Only with project debug enabled.
spent_init?: number;
// Public API discovery loading time in seconds. Only with project debug
// enabled.
unauth_spent?: number;
// Profile and auth preparation time in seconds. Debug only; zero for guests.
auth_spent?: number;
}
What to build with init
Each field links to its data reference, actual consumers and related component guides.
page_data.init/api/apploadLanguage, currencies and units
Languages, currencies and measurement units, including names, display formats and defaults for each locale.
localeuseOrionplace(selectLocales)MainLanguageSwitcherselects the language; the store applies its default currency and units to the current selections.currenciesuseOrionplace(selectCurrencies)Currency choices read by
MainCurrencySwitcher.weightsuseOrionplace(selectInit)?.weightsWeight-unit choices read by
UnitSwitcher.lengthsuseOrionplace(selectInit)?.lengthsLength-unit choices read by
UnitSwitcher.shipping_countriesuseOrionplace(selectInit)?.shipping_countriesThe shop's shipping countries, named in the product pages' return terms.
Catalog and search
Categories, initial product collections, search suggestions and subscription tariffs.
cataloguseOrionplace(selectCatalog)Category tree used by
CatalogHeaderandCategoryGroup.productsuseOrionplace(selectProducts)Initial products read by
ProductGroup,ProductCardand product components such asHoverImageViewer.featureduseOrionplace(selectInit)?.featuredPersonalized recommendations based on previous searches and categories. Product components read them with
collection="featured".sellersuseOrionplace(selectInit)?.sellersSeller details keyed by ID. Each Application Load product identifies its seller with
seller_id; the product store resolves that reference when components read the product.search_hintsuseOrionplace(selectInit)?.search_hintsLocalized suggestions displayed by
MainSearch.tariffsuseOrionplace(selectTariffs)Tariff collection read by
TariffCardsto render individualTariffCardcomponents.
Branding and page metadata
Logos, the site icon, storefront display settings and SEO metadata.
logosuseOrionplace(selectInit)?.logosBrandLogosdisplays the selected logo size or icon.visual_settingsuseOrionplace(selectInit)?.visual_settingsCategory menu images, home category count and ordering, and other storefront display settings.
seouseOrionplace(selectInit)?.seogetStorefrontSeo()prepares metadata forapp/pages/index.vue: the title, description and keywords useuseSeoMeta(), while additional tags useuseHead().
Content and contacts
Banners, storefront events, company contacts and social links.
main_banneruseOrionplace(selectInit)?.main_bannerBanner images displayed by
MainBannerin the current storefront language.eventsuseOrionplace(selectInit)?.eventsEvent images displayed by
EventsBlock, filtered by language andis_layout.contactuseOrionplace(selectInit)?.contactCompany contacts and registration details displayed by
AboutPage.smuseOrionplace(selectInit)?.smSocial links and localized captions displayed by
SocialLinks.
Customer and counters
Authentication status, wallet balance and initial cart and wishlist counters. The full cart and account data are available through separate API endpoints.
is_authenticateduseOrionplace(selectIsAuthenticated)store.isAuthenticatedcontrols authenticated discovery and switchesStorefrontAccountbetween Login and the profile/wallet menu.walletuseOrionplace(selectInit)?.walletWalletBalancedisplays the selected currency balance, including compact mode inStorefrontAccount.variablesuseOrionplace(selectInit)?.variablesInitial counts for the cart and wishlist buttons, preorder permission and content-status defaults.
Maps and integrations
Map center, public map key and availability of address lookup from coordinates.
default_mapuseOrionplace(selectInit)?.default_mapDefault map center coordinates.
geo_location_enableduseOrionplace(selectInit)?.geo_location_enabledIndicates whether address lookup by coordinates is available. Text suggestions depend on the selected country.
public_keysuseOrionplace(selectInit)?.public_keysmap_keyloads Yandex Maps for the delivery map in the cart.
Fields depend on store settings and the current session. A field may be absent or a collection empty. Initial products do not replace a complete, paginated catalog.
How the data reaches components
useOrionplace(selector)- Subscribes a component to the selected part of the state and re-renders it only when that part changes.
useOrionplaceStore()- The store itself: for actions and one-off reads outside render (
getState()). selectResult- The whole request result: the HTTP status and the server response.
selectData- The
page_dataobject of the response. selectInit- The initial data
page_data.init;nulluntil a load has finished. selectProfile- The customer profile after sign-in, otherwise
null. selectStatus · selectIsReady · selectFailure- The load state: in progress, ready, or a failure with its kind and fields.
proxy.tsThe Next.js proxy passes the address of the open page to the server render and keeps personal pages out of caches.
Source code
import { NextResponse, type NextRequest } from 'next/server'; import { marketplaceNoindexPath } from '@orionplace-runtime/marketplace.mjs'; import { APP_LOAD_HANDOFF_HEADER, startAppLoad, } from '@/lib/orionplace/appLoadServer'; /** * A full page load: not an in-app navigation (`RSC`), a prefetch or a public * file such as an image. */ function pageLoad(request: NextRequest) { const accept = request.headers.get('accept') ?? ''; return ( request.method === 'GET' && !request.headers.has('rsc') && !request.headers.has('next-router-prefetch') && !/\.[a-z0-9]+$/i.test(request.nextUrl.pathname) && (!accept || accept.includes('text/html') || accept.includes('*/*')) ); } /** * Personalized pages are never shared between visitors or storefront domains. * Next.js manages its own `Vary` header for pages; the API gateway sets the * storefront-domain `Vary` on its own responses. * * A full page load starts Application Load here and hands it to the render: * when it fails, the error page answers HTTP 503 like the Nuxt and SvelteKit * starters (an unknown address keeps its 404). */ export async function proxy(request: NextRequest) { const forwarded = new Headers(request.headers); // Only this proxy names a started load; a visitor cannot pick one. forwarded.delete(APP_LOAD_HANDOFF_HEADER); forwarded.set( 'x-orionplace-page-url', request.nextUrl.pathname + request.nextUrl.search, ); let unavailable = false; if (pageLoad(request)) { const { id, load } = startAppLoad(forwarded); forwarded.set(APP_LOAD_HANDOFF_HEADER, id); unavailable = Boolean((await load).failure); } const response = NextResponse.next({ request: { headers: forwarded }, ...(unavailable ? { status: 503 } : {}), }); response.headers.set('Cache-Control', 'private, no-store'); // Pages under ORIONPLACE_NOINDEX_PATHS stay out of search engines. if (marketplaceNoindexPath(request.nextUrl.pathname)) { response.headers.set('X-Robots-Tag', 'noindex, follow'); } return response; } // The gateway sets its own headers for /api, /ps3 media, robots.txt and the // sitemap (public blog images keep the backend's browser cache rule); static // assets keep their caching. export const config = { matcher: ['/((?!api/|ps3/|_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml).*)'], };app/api/[...orionplace]/route.tsThe gateway: every browser request to
/api/...goes from here to your marketplace API with the storefront domain and the session cookies.Source code
// Every browser request to /api/... reaches your personal marketplace API here. import { forwardMarketplace } from '@orionplace-runtime/marketplace.mjs'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; function handler(request: Request): Promise<Response> { return forwardMarketplace(request, { mimicFrontDomain: process.env.MIMIC_FRONT_DOMAIN ?? '', }); } export { handler as GET, handler as HEAD, handler as OPTIONS, handler as POST, handler as PUT, handler as PATCH, handler as DELETE, };lib/orionplace/appLoadServer.tsgetOrionplaceAppLoad()calls the same gateway on the server, once per render. A failure is not thrown; it goes to the store.Source code
import { cache } from 'react'; import { headers } from 'next/headers'; import { forwardMarketplace } from '@orionplace-runtime/marketplace.mjs'; import { appLoadRejectedLocale, appLoadResult, inspectAppLoad, } from '@/lib/orionplace/appLoadResult'; import type { AppLoadFailure, AppLoadRequest, AppLoadResult, } from '@/interfaces/orionplace/transport'; /** Server-side bootstrap. Never import this module from a client component. */ export interface ServerAppLoad { result: AppLoadResult | null; failure: AppLoadFailure | null; /** Raw backend `Set-Cookie` headers; only a route handler or server action may relay them. */ cookies: string[]; } /** Headers a server render forwards to the marketplace gateway. */ const FORWARDED = [ 'cookie', 'x-front-domain', 'x-forwarded-host', 'x-forwarded-proto', 'host', 'accept-language', 'user-agent', 'x-init-ip', 'x-real-ip', 'x-forwarded-for', 'x-tz', ]; /** Header the proxy sets on a page request whose Application Load it started. */ export const APP_LOAD_HANDOFF_HEADER = 'x-orionplace-appload'; /** How long a started Application Load waits for the layout to take it. */ const HANDOFF_TTL = 60_000; const HANDOFF_KEY = Symbol.for('orionplace.appLoadHandoff'); /** * Started loads by id. Kept on `globalThis`: the proxy and the page render run * in the same Node process but in separate bundles. */ function handoffs(): Map<string, Promise<ServerAppLoad>> { const scope = globalThis as typeof globalThis & { [HANDOFF_KEY]?: Map<string, Promise<ServerAppLoad>>; }; scope[HANDOFF_KEY] ??= new Map(); return scope[HANDOFF_KEY]; } function buildRequest( incoming: Headers, signal: AbortSignal, withLocale = true, ): Request { const host = incoming.get('x-forwarded-host') || incoming.get('host') || 'localhost'; const protocol = incoming.get('x-forwarded-proto')?.split(',')[0]?.trim() || 'https'; const url = new URL('/api/appload', `${protocol}://${host}`); const pageUrl = new URL( incoming.get('x-orionplace-page-url') || '/', url.origin, ); const locale = withLocale ? pageUrl.searchParams.get('locale') : null; if (locale) { url.searchParams.set('lang', locale); } const forward = new Headers(); for (const name of FORWARDED) { const value = incoming.get(name); if (value) { forward.set(name, value); } } forward.set('accept', 'application/json'); forward.set('content-type', 'application/json'); // Same-origin server request: the gateway rejects cross-site mutations by Origin. forward.set('origin', url.origin); return new Request(url, { method: 'POST', headers: forward, body: JSON.stringify({ detalize_mode: 'light', ignore_cache: false, } satisfies AppLoadRequest), signal, }); } /** * Calls the same gateway the browser uses, in process, with this request's domain * and session. It never throws: the failure is stored for the interface to handle. */ export async function requestAppLoad( signal?: AbortSignal, incoming?: Headers, withLocale = true, ): Promise<ServerAppLoad> { const timeout = AbortSignal.timeout(35_000); const combined = signal ? AbortSignal.any([signal, timeout]) : timeout; let response: Response; let source: Headers; let request: Request; try { source = incoming ?? new Headers(await headers()); request = buildRequest(source, combined, withLocale); response = await forwardMarketplace(request, { mimicFrontDomain: process.env.MIMIC_FRONT_DOMAIN ?? '', }); } catch { return { result: null, failure: { kind: 'network', httpStatus: null, fields: [] }, cookies: [], }; } const cookies = response.headers.getSetCookie(); let body: unknown = null; try { body = await response.json(); } catch { body = null; } const failure = inspectAppLoad(body, response.status); // An address with a language the storefront does not have (`?locale=xx`) // opens in the storefront's default language instead of the 503 page. if ( failure && new URL(request.url).searchParams.has('lang') && appLoadRejectedLocale(failure) ) { return requestAppLoad(signal, source, false); } if (failure) { return { result: null, failure, cookies }; } return { result: appLoadResult(body, response.status), failure: null, cookies, }; } /** * Starts Application Load for a page request in `proxy.ts`, before the page * renders, so a failure can answer HTTP 503 (a page cannot change its status * once it streams). Returns the id to pass in `APP_LOAD_HANDOFF_HEADER`; the * render takes the same result, so the page still costs one request. */ export function startAppLoad(incoming: Headers): { id: string; load: Promise<ServerAppLoad>; } { const id = crypto.randomUUID(); const load = requestAppLoad(undefined, incoming); const store = handoffs(); store.set(id, load); setTimeout(() => store.delete(id), HANDOFF_TTL).unref?.(); return { id, load }; } /** * One request per server render, shared by the layout and any server component * that awaits it. Reading it in a layout initializes the marketplace before the * page renders; it never opens an authentication prompt. A load the proxy * started for this request is taken once; otherwise the render requests it. */ export const getOrionplaceAppLoad = cache(async (): Promise<ServerAppLoad> => { const incoming = await headers(); const id = incoming.get(APP_LOAD_HANDOFF_HEADER); const started = id ? handoffs().get(id) : undefined; if (id && started) { handoffs().delete(id); return started; } return requestAppLoad(undefined, new Headers(incoming)); });app/layout.tsxThe root layout awaits Application Load before the page renders and passes the result to
OrionplaceProvider.Source code
import type { Metadata } from 'next'; import type { ReactNode } from 'react'; import { OrionplaceProvider } from '@/providers/OrionplaceProvider'; import { getOrionplaceAppLoad } from '@/lib/orionplace/appLoadServer'; import { StorefrontShell } from '@/app/StorefrontShell'; import { getOrionplaceServerLocale } from '@/lib/orionplace/serverLocale'; import { getStorefrontMetadata, getStorefrontMetadataContext, } from '@/lib/orionplace/serverMetadata'; import '@/app/globals.css'; export async function generateMetadata(): Promise<Metadata> { return getStorefrontMetadata(); } // Initialization depends on the request domain and session, so pages are dynamic. export const dynamic = 'force-dynamic'; export default async function RootLayout({ children, }: { children: ReactNode; }) { // Bootstrap precedes page rendering. Failures stay in the store for the interface to handle. const { result, failure } = await getOrionplaceAppLoad(); const language = await getOrionplaceServerLocale(); const { tags, jsonLd } = await getStorefrontMetadataContext(); return ( <html lang={language}> <head> {tags.map((tag, index) => 'property' in tag ? ( <meta key={index} property={tag.property} content={tag.content} /> ) : ( <meta key={index} name={tag.name} content={tag.content} /> ), )} {jsonLd ? ( <script type="application/ld+json" // Escaped by structuredDataJson, so the text cannot close the tag. dangerouslySetInnerHTML={{ __html: jsonLd }} /> ) : null} </head> <body> <OrionplaceProvider result={result} failure={failure} locale={language} > <StorefrontShell>{children}</StorefrontShell> </OrionplaceProvider> </body> </html> ); }providers/OrionplaceProvider.tsxCreates the store once per browser session and provides
useOrionplace,useOrionplaceStoreand the hooks of the related stores.Source code
'use client'; import { createContext, useContext, useRef, useEffect, type ReactNode, } from 'react'; import { useStore } from 'zustand'; import { type OrionplaceState, type OrionplaceStore, } from '@/stores/orionplace'; import { type OrionplaceSelectionsState, type OrionplaceSelectionsStore, } from '@/stores/orionplaceSelections'; import { type OrionplaceProductsState, type OrionplaceProductsStore, } from '@/stores/orionplaceProducts'; import { type OrionplaceCatalogState, type OrionplaceCatalogStore, createOrionplaceCatalogStore, } from '@/stores/orionplaceCatalog'; import { type OrionplaceBrowseState, type OrionplaceBrowseStore, } from '@/stores/orionplaceBrowse'; import { createOrionplaceStores, type OrionplaceStores, } from '@/stores/orionplaceScope'; import type { AppLoadFailure, AppLoadResult } from '@/interfaces/orionplace'; import type { IAppLoadCommon } from '@/interfaces/orionplace/appBase/IAppLoadCommon'; import { selectInit } from '@/stores/orionplace'; const OrionplaceContext = createContext<OrionplaceStore | null>(null); const SelectionsContext = createContext<OrionplaceSelectionsStore | null>(null); const ProductsContext = createContext<OrionplaceProductsStore | null>(null); const CatalogContext = createContext<OrionplaceCatalogStore | null>(null); const BrowseContext = createContext<OrionplaceBrowseStore | null>(null); export interface OrionplaceProviderProps { /** The server render's snapshot. Hydration reuses it instead of loading again. */ result?: AppLoadResult | null; failure?: AppLoadFailure | null; locale?: string; children: ReactNode; } /** Creates one store for this browser session and keeps it across re-renders. */ export function OrionplaceProvider({ result = null, failure = null, locale, children, }: OrionplaceProviderProps) { const stores = useRef<OrionplaceStores>(null); if (!stores.current) { stores.current = createOrionplaceStores(result, failure, locale); } const current = stores.current; return ( <OrionplaceContext.Provider value={current.store}> <SelectionsContext.Provider value={current.selections}> <ProductsContext.Provider value={current.products}> <CatalogContext.Provider value={current.catalog}> <BrowseContext.Provider value={current.browse}> {children} </BrowseContext.Provider> </CatalogContext.Provider> </ProductsContext.Provider> </SelectionsContext.Provider> </OrionplaceContext.Provider> ); } export function OrionplaceCatalogSnapshotProvider({ snapshot, children, }: { snapshot?: NonNullable<IAppLoadCommon['tariffs']>; children: ReactNode; }) { const init = useOrionplace(selectInit); const parent = useOrionplaceCatalogStore(); const catalog = useRef<OrionplaceCatalogStore | null>(null); if (!catalog.current) { catalog.current = createOrionplaceCatalogStore(init); if (snapshot) { catalog.current.getState().setTariffs(snapshot); } } const current = catalog.current; useEffect(() => { current.getState().reconcile(init); if (snapshot) { current.getState().setTariffs(snapshot); } }, [current, init, snapshot]); return ( <CatalogContext.Provider value={snapshot ? current : parent}> {children} </CatalogContext.Provider> ); } function useStoreApi(): OrionplaceStore { const store = useContext(OrionplaceContext); if (!store) { throw new Error('Render this component inside <OrionplaceProvider>.'); } return store; } /** Subscribes to one selected part of the marketplace state. */ export function useOrionplace<T>(selector: (state: OrionplaceState) => T): T { return useStore(useStoreApi(), selector); } /** The store itself, for actions and one-off reads outside render. */ export const useOrionplaceStore = useStoreApi; function useSelectionsStoreApi(): OrionplaceSelectionsStore { const store = useContext(SelectionsContext); if (!store) { throw new Error('Render this component inside <OrionplaceProvider>.'); } return store; } export function useOrionplaceSelections<T>( selector: (state: OrionplaceSelectionsState) => T, ): T { return useStore(useSelectionsStoreApi(), selector); } export const useOrionplaceSelectionsStore = useSelectionsStoreApi; function useProductsStoreApi(): OrionplaceProductsStore { const store = useContext(ProductsContext); if (!store) { throw new Error('Render this component inside <OrionplaceProvider>.'); } return store; } export function useOrionplaceProducts<T>( selector: (state: OrionplaceProductsState) => T, ): T { return useStore(useProductsStoreApi(), selector); } export const useOrionplaceProductsStore = useProductsStoreApi; function useCatalogStoreApi(): OrionplaceCatalogStore { const store = useContext(CatalogContext); if (!store) { throw new Error('Render this component inside <OrionplaceProvider>.'); } return store; } export function useOrionplaceCatalog<T>( selector: (state: OrionplaceCatalogState) => T, ): T { return useStore(useCatalogStoreApi(), selector); } export const useOrionplaceCatalogStore = useCatalogStoreApi; function useBrowseStoreApi(): OrionplaceBrowseStore { const store = useContext(BrowseContext); if (!store) { throw new Error('Render this component inside <OrionplaceProvider>.'); } return store; } export function useOrionplaceBrowse<T>( selector: (state: OrionplaceBrowseState) => T, ): T { return useStore(useBrowseStoreApi(), selector); } export const useOrionplaceBrowseStore = useBrowseStoreApi;stores/orionplace.tsThe store with the response and the selectors for its parts:
selectInit,selectCurrencies,selectProductsand more.Source code
import { createStore } from 'zustand/vanilla'; import type { AppLoadFailure, AppLoadLoader, AppLoadResult, AppLoadStatus, IAppLoad, IAppLoadCommon, IAppLoadCommonCurrency, IAppLoadCommonLocale, IAppLoadCommonTariff, IAppLoadPath, IAppLoadPathObject, IApploadUserProfile, ICategory, IProduct, } from '@/interfaces/orionplace'; import { appLoadFailure } from '@/lib/orionplace/appLoadResult'; import { resolveAppLoadProducts } from '@/lib/orionplace/productSeller'; import { guestAppLoadResult } from '@/lib/orionplace/guestSession'; /** Shared empty values keep selector results referentially stable between renders. */ const EMPTY_RECORD = Object.freeze({}) as Record<string, never>; const EMPTY_LIST = Object.freeze([]) as readonly never[]; const EMPTY_PATHS = Object.freeze({}) as IAppLoadPathObject; export interface OrionplaceSnapshot { result: AppLoadResult | null; status: AppLoadStatus; failure: AppLoadFailure | null; endpoints: IAppLoadPathObject; } export interface OrionplaceState extends OrionplaceSnapshot { load: ( loader: AppLoadLoader, force?: boolean, preserve?: boolean, ) => Promise<void>; endSession: (loader: AppLoadLoader) => Promise<void>; patchProfile: (patch: Partial<IApploadUserProfile>) => void; patchWallet: (wallet: NonNullable<IAppLoadCommon['wallet']>) => void; reset: () => void; } export type OrionplaceStore = ReturnType<typeof createOrionplaceStore>; /** Public and authenticated discovery are merged once per snapshot, without mutating the response. */ function mergeEndpoints(result: AppLoadResult | null): IAppLoadPathObject { const page = result?.response.page_data; if (!page) { return EMPTY_PATHS; } const groups: IAppLoadPathObject[] = [page.unauth.paths]; if (page.init.is_authenticated && page.auth) { groups.push(page.auth.paths); } const merged: IAppLoadPathObject = {}; for (const group of groups) { for (const [section, paths] of Object.entries(group)) { merged[section] = { ...merged[section], ...paths }; } } return merged; } /** Builds the hydration snapshot from a server render, without repeating the request. */ export function initialSnapshot( result: AppLoadResult | null = null, failure: AppLoadFailure | null = null, ): OrionplaceSnapshot { return { result: failure ? null : result, status: failure ? 'error' : result ? 'success' : 'idle', failure, endpoints: failure ? EMPTY_PATHS : mergeEndpoints(result), }; } /** * One store instance per server render and one per browser session. * Creating the store never starts a request; the loaders are passed to `load`. */ export function createOrionplaceStore( snapshot: OrionplaceSnapshot = initialSnapshot(), ) { // These belong to this store instance, never serialized state or a module-level cache. let pending: Promise<void> | null = null; let controller: AbortController | null = null; let generation = 0; return createStore<OrionplaceState>()((set, get) => ({ ...snapshot, patchProfile(patch) { set((state) => { const page = state.result?.response.page_data; if (!state.result || !page?.user) { return {}; } return { result: { ...state.result, response: { ...state.result.response, page_data: { ...page, user: { ...page.user, ...patch }, }, }, }, }; }); }, // A purchase changes only the balance: it is replaced in place, without // an Application Load that would unmount the page and its confirmation. patchWallet(wallet) { set((state) => { const page = state.result?.response.page_data; if (!state.result || !page?.init?.is_authenticated) { return {}; } return { result: { ...state.result, response: { ...state.result.response, page_data: { ...page, init: { ...page.init, wallet }, }, }, }, }; }); }, reset() { generation++; controller?.abort(); controller = null; pending = null; set({ result: null, failure: null, status: 'idle', endpoints: EMPTY_PATHS, }); }, endSession(loader) { const result = get().result; if (!result?.response.page_data.init.is_authenticated) { return pending ?? Promise.resolve(); } generation++; controller?.abort(); controller = null; pending = null; const guest = guestAppLoadResult(result); set({ result: guest, endpoints: mergeEndpoints(guest) }); return get().load(loader, true, true); }, load( loader: AppLoadLoader, force = false, preserve = false, ): Promise<void> { if (pending) { return pending; } // Reuse both server success and failure after hydration or navigation; retry is explicit. if (!force && get().status !== 'idle') { return Promise.resolve(); } const current = ++generation; controller = new AbortController(); const signal = controller.signal; if (!preserve) { set({ result: null, failure: null, status: 'pending', endpoints: EMPTY_PATHS, }); } pending = Promise.resolve() .then(() => loader(signal)) .then((result) => { if (current !== generation) { return; } set({ result, failure: null, status: 'success', endpoints: mergeEndpoints(result), }); }) .catch((error: unknown) => { if (current !== generation) { return; } set({ result: null, endpoints: EMPTY_PATHS, failure: appLoadFailure(error), status: 'error', }); }) .finally(() => { if (current !== generation) { return; } pending = null; controller = null; }); return pending; }, })); } export const selectIsReady = (state: OrionplaceSnapshot): boolean => state.status === 'success' && state.result !== null; export const selectStatus = (state: OrionplaceSnapshot): AppLoadStatus => state.status; export const selectFailure = ( state: OrionplaceSnapshot, ): AppLoadFailure | null => state.failure; export const selectResult = (state: OrionplaceSnapshot): AppLoadResult | null => state.result; export const selectData = (state: OrionplaceSnapshot): IAppLoad | null => state.result?.response.page_data ?? null; export const selectInit = (state: OrionplaceSnapshot): IAppLoadCommon | null => selectData(state)?.init ?? null; export const selectProfile = ( state: OrionplaceSnapshot, ): IApploadUserProfile | null => selectData(state)?.user ?? null; export const selectIsAuthenticated = (state: OrionplaceSnapshot): boolean => selectInit(state)?.is_authenticated ?? false; export const selectLocales = ( state: OrionplaceSnapshot, ): Record<string, IAppLoadCommonLocale> => selectInit(state)?.locale ?? EMPTY_RECORD; export const selectCurrencies = ( state: OrionplaceSnapshot, ): Record<string, IAppLoadCommonCurrency> => selectInit(state)?.currencies ?? EMPTY_RECORD; export const selectCatalog = ( state: OrionplaceSnapshot, ): readonly ICategory[] => selectInit(state)?.catalog ?? EMPTY_LIST; export const selectProducts = ( state: OrionplaceSnapshot, ): readonly IProduct[] => { const init = selectInit(state); return init ? resolveAppLoadProducts(init.products, init.sellers) : EMPTY_LIST; }; export const selectTariffs = ( state: OrionplaceSnapshot, ): IAppLoadCommonTariff | null => selectInit(state)?.tariffs ?? null; export const selectPublicEndpoints = ( state: OrionplaceSnapshot, ): IAppLoadPathObject => selectData(state)?.unauth.paths ?? EMPTY_PATHS; export const selectEndpoints = ( state: OrionplaceSnapshot, ): IAppLoadPathObject => state.endpoints; export const selectAuthenticatedEndpoints = ( state: OrionplaceSnapshot, ): IAppLoadPathObject => selectIsAuthenticated(state) ? (selectData(state)?.auth?.paths ?? EMPTY_PATHS) : EMPTY_PATHS; /** Live discovery describes availability; the backend still checks permissions on every call. */ export const selectEndpoint = (section: string, name: string) => (state: OrionplaceSnapshot): IAppLoadPath | undefined => state.endpoints[section]?.[name];stores/orionplaceSelections.tsThe visitor’s choices: language, currency, weight and length units.
Source code
import { createStore } from 'zustand/vanilla'; import type { IAppLoadCommon } from '@/interfaces/orionplace'; export interface OrionplaceSelectionsState { localeCode: string; currencyId: string; weightId: string; lengthId: string; init: IAppLoadCommon | null; setLocale: (code: string) => void; setCurrency: (id: string) => void; setUnit: (kind: 'weights' | 'lengths', id: string) => void; reconcile: (init: IAppLoadCommon | null) => void; reset: () => void; } export type OrionplaceSelectionsStore = ReturnType< typeof createOrionplaceSelectionsStore >; function findLocale(init: IAppLoadCommon, code: string) { return Object.entries(init.locale).find( ([key, value]) => (value.code?.trim() || key).toLowerCase() === code.trim().toLowerCase(), ); } function unitId(values: Record<string, unknown>, preferred?: number | string) { const id = String(preferred ?? ''); return Object.hasOwn(values, id) ? id : (Object.keys(values)[0] ?? ''); } function availableCurrency(init: IAppLoadCommon, preferred?: number | string) { const id = String(preferred ?? ''); if (init.currencies[id]?.allowed_select) { return id; } return Object.keys(init.currencies).find( (key) => init.currencies[key]?.allowed_select, ) ?? ''; } export function initialSelections(init: IAppLoadCommon | null, locale = '') { const selected = init ? findLocale(init, locale) ?? Object.entries(init.locale)[0] : undefined; return { localeCode: selected?.[1].code?.trim() || selected?.[0] || '', currencyId: init ? availableCurrency(init, selected?.[1].currency) : '', weightId: init ? unitId(init.weights ?? {}, selected?.[1].weight) : '', lengthId: init ? unitId(init.lengths ?? {}, selected?.[1].length) : '', }; } /** One customer's choices; created per server render and browser session. */ export function createOrionplaceSelectionsStore( init: IAppLoadCommon | null = null, locale = '', ) { return createStore<OrionplaceSelectionsState>()((set, get) => ({ ...initialSelections(init, locale), init, setLocale(code) { const current = get().init; const entry = current && findLocale(current, code); if (!current || !entry) { return; } set({ localeCode: entry[1].code?.trim() || entry[0], currencyId: availableCurrency(current, entry[1].currency), weightId: unitId(current.weights ?? {}, entry[1].weight), lengthId: unitId(current.lengths ?? {}, entry[1].length), }); }, setCurrency(id) { if (get().init?.currencies[id]?.allowed_select) { set({ currencyId: id }); } }, setUnit(kind, id) { const values = get().init?.[kind]; if (!values || !Object.hasOwn(values, id)) { return; } set(kind === 'weights' ? { weightId: id } : { lengthId: id }); }, reconcile(next) { if (!next) { set({ ...initialSelections(null), init: null }); return; } const previous = get(); const selected = findLocale(next, previous.localeCode) ?? Object.entries(next.locale)[0]; const localeCode = selected?.[1].code?.trim() || selected?.[0] || ''; const preferredCurrency = next.currencies[previous.currencyId] ?.allowed_select ? previous.currencyId : selected?.[1].currency; set({ init: next, localeCode, currencyId: availableCurrency(next, preferredCurrency), weightId: unitId( next.weights ?? {}, Object.hasOwn(next.weights ?? {}, previous.weightId) ? previous.weightId : selected?.[1].weight, ), lengthId: unitId( next.lengths ?? {}, Object.hasOwn(next.lengths ?? {}, previous.lengthId) ? previous.lengthId : selected?.[1].length, ), }); }, reset() { set({ ...initialSelections(null), init: null }); }, })); }hooks/useOrionplaceAppLoad.tsBrowser actions:
initialize,refreshandreset. The hook itself sends no request; after sign-in or sign-out callreset, thenrefresh.Source code
'use client'; import { useCallback } from 'react'; import { useOrionplace, useOrionplaceStore, useOrionplaceSelectionsStore, } from '@/providers/OrionplaceProvider'; import { requestBrowserAppLoad } from '@/lib/orionplace/appLoadClient'; import { selectFailure, selectIsReady, selectResult, selectStatus, } from '@/stores/orionplace'; import type { AppLoadFailure, AppLoadResult, AppLoadStatus, } from '@/interfaces/orionplace'; export interface OrionplaceAppLoad { result: AppLoadResult | null; status: AppLoadStatus; failure: AppLoadFailure | null; isReady: boolean; initialize: () => Promise<void>; refresh: () => Promise<void>; reset: () => void; } /** * Explicit browser actions. Calling this hook does not start a request: the * server render initializes the marketplace and hydration reuses that snapshot. * After a confirmed login or logout, call reset and then refresh. */ export function useOrionplaceAppLoad(): OrionplaceAppLoad { const store = useOrionplaceStore(); const selections = useOrionplaceSelectionsStore(); const result = useOrionplace(selectResult); const status = useOrionplace(selectStatus); const failure = useOrionplace(selectFailure); const isReady = useOrionplace(selectIsReady); const initialize = useCallback( () => store .getState() .load((signal) => requestBrowserAppLoad(signal, selections.getState().localeCode), ), [store, selections], ); const refresh = useCallback( () => store .getState() .load( (signal) => requestBrowserAppLoad(signal, selections.getState().localeCode), true, ), [store, selections], ); const reset = useCallback(() => store.getState().reset(), [store]); return { result, status, failure, isReady, initialize, refresh, reset }; }lib/orionplace/appLoadClient.tsThe browser request through the same-site gateway; the browser stores and sends the session cookies itself.
Source code
import { AppLoadError, appLoadRejectedLocale, appLoadResult, inspectAppLoad, } from '@/lib/orionplace/appLoadResult'; import type { AppLoadRequest, AppLoadResult, } from '@/interfaces/orionplace/transport'; import { APP_ORIGIN, nativeAppRequest } from '@/lib/orionplace/nativeApp'; function withTimeout(signal: AbortSignal, milliseconds: number): AbortSignal { if (typeof AbortSignal.any !== 'function') { return signal; } return AbortSignal.any([signal, AbortSignal.timeout(milliseconds)]); } /** * Browser transport for explicit initialize and refresh actions. The browser * sends and stores session cookies itself through the same-origin gateway; * the Android app sends the request to its storefront (nativeApp.ts). */ export async function requestBrowserAppLoad( signal: AbortSignal, locale = '', ): Promise<AppLoadResult> { const path = locale ? `/api/appload?lang=${encodeURIComponent(locale)}` : '/api/appload'; const headers = { Accept: 'application/json', 'Content-Type': 'application/json', }; const request = { detalize_mode: 'light', ignore_cache: false, } satisfies AppLoadRequest; let status: number; let body: unknown = null; try { if (APP_ORIGIN) { const response = await nativeAppRequest(APP_ORIGIN, path, { method: 'POST', headers, data: request, signal: withTimeout(signal, 35_000), }); status = response.status; body = response.body; } else { const response = await fetch(path, { method: 'POST', headers, body: JSON.stringify(request), credentials: 'same-origin', cache: 'no-store', signal: withTimeout(signal, 35_000), }); status = response.status; body = await response.json().catch(() => null); } } catch { throw new AppLoadError('Could not reach the marketplace API.', { kind: 'network', httpStatus: null, fields: [], }); } const failure = inspectAppLoad(body, status); // A language the storefront does not have opens its default language. if (failure && locale && appLoadRejectedLocale(failure)) { return requestBrowserAppLoad(signal); } if (failure) { throw new AppLoadError('Marketplace initialization failed.', failure); } return appLoadResult(body, status); }lib/orionplace/appLoadResult.tsChecks the response and turns network and server errors into one safe shape for the store.
Source code
import type { AppLoadFailure, AppLoadResponse, AppLoadResult, } from '@/interfaces/orionplace/transport'; const record = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === 'object' && !Array.isArray(value); /** Validate the bootstrap boundary. Interfaces describe nested data, not a runtime schema. */ export function inspectAppLoad( body: unknown, httpStatus: number, ): AppLoadFailure | null { if (httpStatus < 200 || httpStatus >= 300) { return { kind: 'http', httpStatus, fields: [] }; } if ( record(body) && record(body.form_errors) && Object.keys(body.form_errors).length ) { return { kind: 'validation', httpStatus, fields: Object.keys(body.form_errors), }; } const page = record(body) && body.page_data; const init = record(page) && page.init; const unauth = record(page) && page.unauth; if ( !record(body) || !('form_errors' in body) || (body.form_errors !== null && !record(body.form_errors)) || !record(init) || typeof init.is_authenticated !== 'boolean' || !record(init.locale) || !record(init.currencies) || !record(unauth) || !record(unauth.paths) || Object.values(unauth.paths).some( (group) => !record(group) || Object.values(group).some( (endpoint) => !record(endpoint) || typeof endpoint.target !== 'string' || !Array.isArray(endpoint.methods), ), ) ) { return { kind: 'invalid-response', httpStatus, fields: [] }; } return null; } export function appLoadResult( body: unknown, httpStatus: number, ): AppLoadResult { const failure = inspectAppLoad(body, httpStatus); if (failure) { throw new Error(`AppLoad: ${failure.kind}`); } return { response: body as AppLoadResponse & { page_data: NonNullable<AppLoadResponse['page_data']>; }, httpStatus, loadedAt: new Date().toISOString(), }; } /** Transport and envelope failures reach the store as this sanitized shape. */ export function appLoadFailure(error: unknown): AppLoadFailure { const detail = error && typeof error === 'object' && 'data' in error ? (error as { data: unknown }).data : null; const known = record(detail) && 'kind' in detail && ['http', 'validation', 'invalid-response', 'network'].includes( String(detail.kind), ); if (!known || !record(detail)) { return { kind: 'network', httpStatus: null, fields: [] }; } return { kind: detail.kind as AppLoadFailure['kind'], httpStatus: typeof detail.httpStatus === 'number' ? detail.httpStatus : null, fields: Array.isArray(detail.fields) ? detail.fields.filter( (field): field is string => typeof field === 'string', ) : [], }; } /** Carries a sanitized failure from the transport to the store without a stack or response body. */ export class AppLoadError extends Error { readonly data: AppLoadFailure; constructor(message: string, data: AppLoadFailure) { super(message); this.name = 'AppLoadError'; this.data = data; } } /** * Whether App Load refused the request's language (`?lang=` of a language the * storefront does not have): the backend answers it with field errors and * HTTP 452. The language is the only client value App Load reads, so the * storefront loads again without it. */ export function appLoadRejectedLocale(failure: AppLoadFailure): boolean { return failure.kind === 'validation' || failure.httpStatus === 452; }