Application initialization
Application Load · Svelte
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.
src/lib/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.
localestore.localesMainLanguageSwitcherselects the language; the store applies its default currency and units to the current selections.currenciesstore.currenciesCurrency choices read by
MainCurrencySwitcher.weightsstore.weightsWeight-unit choices read by
UnitSwitcher.lengthsstore.lengthsLength-unit choices read by
UnitSwitcher.shipping_countriesstore.init?.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.
catalogstore.catalogCategory tree used by
CatalogHeaderandCategoryGroup.productsstore.productsInitial products read by
ProductGroup,ProductCardand product components such asHoverImageViewer.featuredstore.featuredPersonalized recommendations based on previous searches and categories. Product components read them with
collection="featured".sellersstore.init?.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_hintsstore.init?.search_hintsLocalized suggestions displayed by
MainSearch.tariffsstore.tariffsTariff collection read by
TariffCardsto render individualTariffCardcomponents.
Branding and page metadata
Logos, the site icon, storefront display settings and SEO metadata.
logosstore.init?.logosBrandLogosdisplays the selected logo size or icon.visual_settingsstore.init?.visual_settingsCategory menu images, home category count and ordering, and other storefront display settings.
seostore.init?.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_bannerstore.init?.main_bannerBanner images displayed by
MainBannerin the current storefront language.eventsstore.init?.eventsEvent images displayed by
EventsBlock, filtered by language andis_layout.contactstore.init?.contactCompany contacts and registration details displayed by
AboutPage.smstore.init?.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_authenticatedstore.isAuthenticatedstore.isAuthenticatedcontrols authenticated discovery and switchesStorefrontAccountbetween Login and the profile/wallet menu.walletstore.init?.walletWalletBalancedisplays the selected currency balance, including compact mode inStorefrontAccount.variablesstore.init?.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_mapstore.init?.default_mapDefault map center coordinates.
geo_location_enabledstore.init?.geo_location_enabledIndicates whether address lookup by coordinates is available. Text suggestions depend on the selected country.
public_keysstore.init?.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
useOrionplaceStore()- The store of the current request from the app context; call it while the component initializes.
store.result- The whole request result: the HTTP status and the server response.
store.data- The
page_dataobject of the response. store.init- The initial data
page_data.init;nulluntil a load has finished. store.profile- The customer profile after sign-in, otherwise
null. store.status · store.isReady · store.failure- The load state: in progress, ready, or a failure with its kind and fields.
src/hooks.server.tsKeeps personal pages out of caches and answers with the 503 error page when Application Load fails.
Source code
import type { Handle } from '@sveltejs/kit'; import { env } from '$env/dynamic/private'; import { marketplaceNoindexPath } from '@orionplace-runtime/marketplace.mjs'; function escapeAttribute(value: string) { return value .replaceAll('&', '&') .replaceAll('"', '"') .replaceAll('<', '<') .replaceAll('>', '>'); } /** * Personalized pages are never shared between visitors or storefront domains. * The API gateway (/api, /ps3 media), robots.txt and the sitemap set their own * headers, so those requests are left alone: public blog images keep the * backend's browser cache rule. */ const ownHeaders = ['/robots.txt', '/sitemap.xml']; const gatewayPaths = ['/api/', '/ps3/']; export const handle: Handle = async ({ event, resolve }) => { const response = await resolve(event, { transformPageChunk: ({ html }) => html.replace( '%orionplace.locale%', escapeAttribute(event.locals.orionplaceLocale ?? 'en'), ), }); if ( !gatewayPaths.some((path) => event.url.pathname.startsWith(path)) && !ownHeaders.includes(event.url.pathname) ) { response.headers.set('Cache-Control', 'private, no-store'); response.headers.set('Vary', 'Host, X-Forwarded-Host, X-Front-Domain'); } // Pages under ORIONPLACE_NOINDEX_PATHS stay out of search engines. if ( marketplaceNoindexPath(event.url.pathname, env.ORIONPLACE_NOINDEX_PATHS) ) { response.headers.set('X-Robots-Tag', 'noindex, follow'); } // A page rendered while Application Load failed is the 503 error page. if ( event.locals.orionplaceUnavailable && !event.isDataRequest && response.status === 200 ) { return new Response(response.body, { status: 503, statusText: 'Service Unavailable', headers: response.headers, }); } return response; };src/routes/api/[...orionplace]/+server.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 { env } from '$env/dynamic/private'; import { forwardMarketplace } from '@orionplace-runtime/marketplace.mjs'; import type { RequestHandler } from './$types'; const handler: RequestHandler = ({ request }) => forwardMarketplace(request, { mimicFrontDomain: env.MIMIC_FRONT_DOMAIN ?? '', }); export const GET = handler; export const HEAD = handler; export const OPTIONS = handler; export const POST = handler; export const PUT = handler; export const PATCH = handler; export const DELETE = handler;src/lib/orionplace/appLoadServer.tsloadOrionplaceAppLoad()calls the same gateway on the server and passes the session cookies of the response to the browser. A failure is not thrown; it goes to the snapshot.Source code
import { env } from '$env/dynamic/private'; import { forwardMarketplace } from '@orionplace-runtime/marketplace.mjs'; import { appLoadRejectedLocale, appLoadResult, inspectAppLoad, } from '@/lib/orionplace/appLoadResult'; import type { OrionplaceSnapshot } from '@/lib/orionplace/appLoadData'; import type { AppLoadRequest } from '@/lib/interfaces/orionplace/transport'; import type { Cookies, RequestEvent } from '@sveltejs/kit'; /** 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', ]; function buildRequest( event: RequestEvent, signal: AbortSignal, withLocale = true, ): Request { const url = new URL('/api/appload', event.url.origin); const locale = withLocale ? event.url.searchParams.get('locale') : null; if (locale) { url.searchParams.set('lang', locale); } const forward = new Headers(); for (const name of FORWARDED) { const value = event.request.headers.get(name); if (value) { forward.set(name, value); } } if (!forward.has('host')) { forward.set('host', event.url.host); } // The visitor's current cookies, including ones set earlier in this render. const cookies = event.cookies.getAll(); if (cookies.length) { forward.set( 'cookie', cookies.map((item) => `${item.name}=${item.value}`).join('; '), ); } 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, }); } /** * Relays each backend cookie separately, preserving its exact signed value. * The gateway already removed the backend `Domain`, binding cookies to this storefront. */ export function relayCookies(cookies: Cookies, received: string[]): void { for (const raw of received) { const [pair, ...attributes] = raw.split(';'); const separator = pair.indexOf('='); if (separator < 1) { continue; } const name = pair.slice(0, separator).trim(); const value = pair.slice(separator + 1).trim(); const options: Parameters<Cookies['set']>[2] = { path: '/', httpOnly: false, secure: false, encode: (raw) => raw, }; for (const attribute of attributes) { const [key, ...rest] = attribute.split('='); const flag = key.trim().toLowerCase(); const detail = rest.join('=').trim(); if (flag === 'path') { options.path = detail || '/'; } else if (flag === 'max-age' && detail) { options.maxAge = Number(detail); } else if (flag === 'expires' && detail) { options.expires = new Date(detail); } else if (flag === 'samesite' && detail) { options.sameSite = detail.toLowerCase() as 'lax' | 'strict' | 'none'; } else if (flag === 'httponly') { options.httpOnly = true; } else if (flag === 'secure') { options.secure = true; } } cookies.set(name, value, options); } } export interface ServerAppLoad extends OrionplaceSnapshot { /** Raw backend `Set-Cookie` headers, already relayed to the visitor. */ cookies: string[]; } /** * Calls the same gateway the browser uses, in process, with this request's domain * and session. It never throws: the failure travels to the interface in the snapshot. */ export async function loadOrionplaceAppLoad( event: RequestEvent, signal?: AbortSignal, withLocale = true, ): Promise<ServerAppLoad> { const timeout = AbortSignal.timeout(35_000); const combined = signal ? AbortSignal.any([signal, timeout]) : timeout; let response: Response; try { response = await forwardMarketplace( buildRequest(event, combined, withLocale), { mimicFrontDomain: env.MIMIC_FRONT_DOMAIN ?? '', }, ); } catch { return { result: null, failure: { kind: 'network', httpStatus: null, fields: [] }, cookies: [], }; } // AppLoad establishes the guest or customer session during a server render. const cookies = response.headers.getSetCookie(); relayCookies(event.cookies, cookies); 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 && withLocale && event.url.searchParams.get('locale') && appLoadRejectedLocale(failure) ) { return loadOrionplaceAppLoad(event, signal, false); } if (failure) { return { result: null, failure, cookies }; } return { result: appLoadResult(body, response.status), failure: null, cookies, }; }src/routes/+layout.server.tsThe root
loadruns Application Load before the page renders and returns the snapshot in the layout data.Source code
import { loadOrionplaceAppLoad } from '@/lib/orionplace/appLoadServer'; import { OrionplaceStore } from '@/lib/orionplace/store.svelte'; import { storefrontErrorLocale } from '@/lib/utils/orionplace/errorPageLabels'; import type { LayoutServerLoad } from './$types'; // Initialization depends on the request domain and session, so pages are dynamic. export const prerender = false; /** Bootstrap precedes page rendering. A failure opens the 503 error page. */ export const load: LayoutServerLoad = async (event) => { // The address is read untracked: a link with another `?locale` switches the // language in StorefrontLayout and does not run Application Load again, // which would drop the products the open page has just loaded. const { result, failure } = await event.untrack(() => loadOrionplaceAppLoad(event), ); const store = new OrionplaceStore({ result, failure }); const locale = event.untrack(() => event.url.searchParams.get('locale'), ); if (locale) { store.setLocale(locale); } if (!store.isReady) { store.localeCode = storefrontErrorLocale(locale, store.localeCode); } event.locals.orionplaceLocale = store.localeCode || 'en'; // The root layout shows the 503 error page; hooks.server.ts sets the status. event.locals.orionplaceUnavailable = store.status === 'error' && event.route.id !== null && !event.route.id.startsWith('/__components/'); return { orionplace: { result, failure }, orionplaceLocale: store.localeCode, }; };src/routes/+layout.svelteThe root layout puts the snapshot into the store (
hydrate) and shows the error page when the load failed.Source code
<script lang="ts"> import { untrack } from 'svelte'; import { computed } from '@/lib/orionplace/reactivity.svelte'; import { page } from '$app/state'; import { replaceState } from '$app/navigation'; import { localesMatch } from '@/lib/utils/orionplace/presentation'; import { APP_ORIGIN, handleAppBackButton, saveAppLocale, } from '@/lib/utils/orionplace/nativeApp'; import { setOrionplaceContext } from '@/lib/orionplace/store.svelte'; import { followAppSession } from '@/lib/orionplace/appSession'; import { providePageHead } from '@/lib/orionplace/navigation.svelte'; import { storefrontErrorKind, storefrontErrorLocale, } from '@/lib/utils/orionplace/errorPageLabels'; import { provideOrionplaceProgress } from '@/lib/composables/useOrionplaceProgress.svelte'; import { useOrionplaceTheme } from '@/lib/composables/useOrionplaceTheme.svelte'; import OrionplaceThemeProvider from '@/lib/components/orionplace/theme/OrionplaceThemeProvider.svelte'; import StorefrontLayout from '@/lib/layouts/StorefrontLayout.svelte'; import StorefrontPageState from '@/lib/layouts/StorefrontPageState.svelte'; import StorefrontPageHead from '@/lib/layouts/StorefrontPageHead.svelte'; import StorefrontUnavailable from '@/lib/layouts/StorefrontUnavailable.svelte'; import '@/lib/assets/css/orionplace.css'; import type { Snippet } from 'svelte'; import type { LayoutData } from './$types'; let { data, children }: { data: LayoutData; children: Snippet } = $props(); const marketplace = setOrionplaceContext(untrack(() => data.orionplace)); // App page loads see sign-in and logout (see lib/orionplace/appSession.ts). if (APP_ORIGIN) { followAppSession(() => marketplace.result ? { result: marketplace.result, failure: null } : null, ); } marketplace.setLocale(untrack(() => data.orionplaceLocale)); if (!marketplace.isReady) { marketplace.localeCode = untrack(() => data.orionplaceLocale); } const head = providePageHead(); const progress = provideOrionplaceProgress(); const { mode } = useOrionplaceTheme(); const metadata = computed(() => head.value?.() ?? {}); const preview = computed( () => page.route.id?.startsWith('/__components/') ?? false, ); const standaloneError = computed( () => page.status >= 400 && (!marketplace.isReady || storefrontErrorKind(page.status) === 'error'), ); // A failed Application Load replaces every existing page with the 503 // error page; an unknown address keeps its 404 page. const unavailable = computed( () => marketplace.status === 'error' && page.route.id !== null, ); $effect(() => { const snapshot = data.orionplace; untrack(() => marketplace.hydrate(snapshot)); }); $effect(() => { if (!marketplace.isReady) { marketplace.localeCode = storefrontErrorLocale( page.url.searchParams.get('locale'), marketplace.localeCode, ); } document.documentElement.lang = marketplace.localeCode; }); // The Android app's back button: close a dialog, go back, or leave the app. $effect(() => { if (APP_ORIGIN) { handleAppBackButton(); } }); // A language the customer picks goes into the address, so a reload and a // shared link keep it; the Android app also keeps it on the device. The // first language set while the storefront loads leaves the address as it is. let shownLocale = untrack(() => marketplace.localeCode); $effect(() => { const code = marketplace.localeCode; const previous = shownLocale; shownLocale = code; untrack(() => { if (APP_ORIGIN && previous && code && previous !== code) { saveAppLocale(code); } // `replaceState` changes the address without updating `page.url`. const url = new URL(location.href); const requested = url.searchParams.get('locale'); if ( !previous || !code || previous === code || !marketplace.isReady || (requested && localesMatch(requested, code)) ) { return; } url.searchParams.set('locale', code); replaceState(url, page.state); }); }); </script> <OrionplaceThemeProvider mode={mode.value} class="min-h-screen" > {#if progress.value > 0} <div class="storefront-progress" role="progressbar" aria-label="Loading" ></div> {/if} {#if preview.value || standaloneError.value} {@render children()} {:else if unavailable.value} <StorefrontUnavailable /> {:else} <StorefrontLayout> {#key page.url.pathname + page.url.search} <StorefrontPageState state={page.data.orionplacePageState}> {@render children()} </StorefrontPageState> {/key} </StorefrontLayout> {/if} </OrionplaceThemeProvider> <!-- Render after the page registers its metadata during server rendering. --> <StorefrontPageHead metadata={metadata.value} icon={marketplace.init?.logos?.ico} /> <style> .storefront-progress { position: fixed; top: 0; left: 0; z-index: 100; width: 100%; height: 2px; background: var(--orionplace-text); animation: storefront-pending 1s ease-in-out infinite; transform-origin: left; } @keyframes storefront-pending { from { transform: scaleX(0.1); } to { transform: scaleX(0.95); } } </style>src/lib/orionplace/store.svelte.tsOne request’s store in the app context, so the data of different visitors never mix on the server.
Source code
import { getContext, setContext } from 'svelte'; import { createOrionplaceStore } from '@/lib/stores/orionplace.svelte'; import { createScope } from '@/lib/orionplace/scope.svelte'; import { snapshotStatus } from '@/lib/orionplace/appLoadData'; import { requestBrowserAppLoad } from '@/lib/orionplace/appLoadClient'; import type { OrionplaceSnapshot } from '@/lib/orionplace/appLoadData'; const ORIONPLACE_KEY = Symbol('orionplace'); export class OrionplaceStore { readonly facade = createOrionplaceStore('orionplace', createScope()); #snapshot: OrionplaceSnapshot | null = null; constructor(snapshot?: OrionplaceSnapshot | null) { this.hydrate(snapshot); } get result() { return this.facade.result; } set result(value: typeof this.facade.result) { this.facade.result = value; } get status() { return this.facade.status; } set status(value: typeof this.facade.status) { this.facade.status = value; } get failure() { return this.facade.failure; } set failure(value: typeof this.facade.failure) { this.facade.failure = value; } get isReady() { return this.facade.isReady; } set isReady(value: typeof this.facade.isReady) { this.facade.isReady = value; } get data() { return this.facade.data; } set data(value: typeof this.facade.data) { this.facade.data = value; } get init() { return this.facade.init; } set init(value: typeof this.facade.init) { this.facade.init = value; } get profile() { return this.facade.profile; } set profile(value: typeof this.facade.profile) { this.facade.profile = value; } get isAuthenticated() { return this.facade.isAuthenticated; } set isAuthenticated(value: typeof this.facade.isAuthenticated) { this.facade.isAuthenticated = value; } get locales() { return this.facade.locales; } set locales(value: typeof this.facade.locales) { this.facade.locales = value; } get currencies() { return this.facade.currencies; } set currencies(value: typeof this.facade.currencies) { this.facade.currencies = value; } get catalog() { return this.facade.catalog; } set catalog(value: typeof this.facade.catalog) { this.facade.catalog = value; } get catalogSnapshot() { return this.facade.catalogSnapshot; } set catalogSnapshot(value: typeof this.facade.catalogSnapshot) { this.facade.catalogSnapshot = value; } get catalogRoots() { return this.facade.catalogRoots; } set catalogRoots(value: typeof this.facade.catalogRoots) { this.facade.catalogRoots = value; } get catalogShortcuts() { return this.facade.catalogShortcuts; } set catalogShortcuts(value: typeof this.facade.catalogShortcuts) { this.facade.catalogShortcuts = value; } get catalogCategories() { return this.facade.catalogCategories; } set catalogCategories(value: typeof this.facade.catalogCategories) { this.facade.catalogCategories = value; } get products() { return this.facade.products; } set products(value: typeof this.facade.products) { this.facade.products = value; } get tariffs() { return this.facade.tariffs; } set tariffs(value: typeof this.facade.tariffs) { this.facade.tariffs = value; } get tariffsSnapshot() { return this.facade.tariffsSnapshot; } set tariffsSnapshot(value: typeof this.facade.tariffsSnapshot) { this.facade.tariffsSnapshot = value; } get storeTariffs() { return this.facade.storeTariffs; } get featured() { return this.facade.featured; } set featured(value: typeof this.facade.featured) { this.facade.featured = value; } get homeNewProductIds() { return this.facade.homeNewProductIds; } set homeNewProductIds(value: typeof this.facade.homeNewProductIds) { this.facade.homeNewProductIds = value; } get homeProductSections() { return this.facade.homeProductSections; } set homeProductSections(value: typeof this.facade.homeProductSections) { this.facade.homeProductSections = value; } get weights() { return this.facade.weights; } set weights(value: typeof this.facade.weights) { this.facade.weights = value; } get lengths() { return this.facade.lengths; } set lengths(value: typeof this.facade.lengths) { this.facade.lengths = value; } get localeCode() { return this.facade.localeCode; } set localeCode(value: typeof this.facade.localeCode) { this.facade.localeCode = value; } get currencyId() { return this.facade.currencyId; } set currencyId(value: typeof this.facade.currencyId) { this.facade.currencyId = value; } get weightId() { return this.facade.weightId; } set weightId(value: typeof this.facade.weightId) { this.facade.weightId = value; } get lengthId() { return this.facade.lengthId; } set lengthId(value: typeof this.facade.lengthId) { this.facade.lengthId = value; } get searchQuery() { return this.facade.searchQuery; } set searchQuery(value: typeof this.facade.searchQuery) { this.facade.searchQuery = value; } get searchFacets() { return this.facade.searchFacets; } set searchFacets(value: typeof this.facade.searchFacets) { this.facade.searchFacets = value; } get searchFilters() { return this.facade.searchFilters; } set searchFilters(value: typeof this.facade.searchFilters) { this.facade.searchFilters = value; } get blogPosts() { return this.facade.blogPosts; } set blogPosts(value: typeof this.facade.blogPosts) { this.facade.blogPosts = value; } get currentLocale() { return this.facade.currentLocale; } set currentLocale(value: typeof this.facade.currentLocale) { this.facade.currentLocale = value; } get currentCurrency() { return this.facade.currentCurrency; } set currentCurrency(value: typeof this.facade.currentCurrency) { this.facade.currentCurrency = value; } get productUpdates() { return this.facade.productUpdates; } set productUpdates(value: typeof this.facade.productUpdates) { this.facade.productUpdates = value; } get productRequests() { return this.facade.productRequests; } set productRequests(value: typeof this.facade.productRequests) { this.facade.productRequests = value; } get counters() { return this.facade.counters; } set counters(value: typeof this.facade.counters) { this.facade.counters = value; } get productSecondaryVariants() { return this.facade.productSecondaryVariants; } set productSecondaryVariants( value: typeof this.facade.productSecondaryVariants, ) { this.facade.productSecondaryVariants = value; } get publicEndpoints() { return this.facade.publicEndpoints; } set publicEndpoints(value: typeof this.facade.publicEndpoints) { this.facade.publicEndpoints = value; } get authenticatedEndpoints() { return this.facade.authenticatedEndpoints; } set authenticatedEndpoints(value: typeof this.facade.authenticatedEndpoints) { this.facade.authenticatedEndpoints = value; } get endpoints() { return this.facade.endpoints; } set endpoints(value: typeof this.facade.endpoints) { this.facade.endpoints = value; } patchProfile = (...args: Parameters<typeof this.facade.patchProfile>) => this.facade.patchProfile(...args); patchWallet = (...args: Parameters<typeof this.facade.patchWallet>) => this.facade.patchWallet(...args); setCatalogTree = (...args: Parameters<typeof this.facade.setCatalogTree>) => this.facade.setCatalogTree(...args); setTariffs = (...args: Parameters<typeof this.facade.setTariffs>) => this.facade.setTariffs(...args); setStoreTariffs = ( ...args: Parameters<typeof this.facade.setStoreTariffs> ) => this.facade.setStoreTariffs(...args); setHomeNewProducts = ( ...args: Parameters<typeof this.facade.setHomeNewProducts> ) => this.facade.setHomeNewProducts(...args); setSearchFacets = (...args: Parameters<typeof this.facade.setSearchFacets>) => this.facade.setSearchFacets(...args); preserveSearchFacets = ( ...args: Parameters<typeof this.facade.preserveSearchFacets> ) => this.facade.preserveSearchFacets(...args); setSearchFilters = ( ...args: Parameters<typeof this.facade.setSearchFilters> ) => this.facade.setSearchFilters(...args); setBlogPosts = (...args: Parameters<typeof this.facade.setBlogPosts>) => this.facade.setBlogPosts(...args); setLocale = (...args: Parameters<typeof this.facade.setLocale>) => this.facade.setLocale(...args); setCurrency = (...args: Parameters<typeof this.facade.setCurrency>) => this.facade.setCurrency(...args); setUnit = (...args: Parameters<typeof this.facade.setUnit>) => this.facade.setUnit(...args); getProducts = (...args: Parameters<typeof this.facade.getProducts>) => this.facade.getProducts(...args); getTariff = (...args: Parameters<typeof this.facade.getTariff>) => this.facade.getTariff(...args); getProduct = (...args: Parameters<typeof this.facade.getProduct>) => this.facade.getProduct(...args); getProductRequest = ( ...args: Parameters<typeof this.facade.getProductRequest> ) => this.facade.getProductRequest(...args); setProductRequest = ( ...args: Parameters<typeof this.facade.setProductRequest> ) => this.facade.setProductRequest(...args); setProductCounters = ( ...args: Parameters<typeof this.facade.setProductCounters> ) => this.facade.setProductCounters(...args); setProduct = (...args: Parameters<typeof this.facade.setProduct>) => this.facade.setProduct(...args); patchProductState = ( ...args: Parameters<typeof this.facade.patchProductState> ) => this.facade.patchProductState(...args); synchronizeProductCart = ( ...args: Parameters<typeof this.facade.synchronizeProductCart> ) => this.facade.synchronizeProductCart(...args); getSecondaryVariants = ( ...args: Parameters<typeof this.facade.getSecondaryVariants> ) => this.facade.getSecondaryVariants(...args); setSecondaryVariants = ( ...args: Parameters<typeof this.facade.setSecondaryVariants> ) => this.facade.setSecondaryVariants(...args); getProductAvailability = ( ...args: Parameters<typeof this.facade.getProductAvailability> ) => this.facade.getProductAvailability(...args); getCatalogCategory = ( ...args: Parameters<typeof this.facade.getCatalogCategory> ) => this.facade.getCatalogCategory(...args); getCatalogProducts = ( ...args: Parameters<typeof this.facade.getCatalogProducts> ) => this.facade.getCatalogProducts(...args); applyResult = (...args: Parameters<typeof this.facade.applyResult>) => this.facade.applyResult(...args); getEndpoint = (...args: Parameters<typeof this.facade.getEndpoint>) => this.facade.getEndpoint(...args); load = (...args: Parameters<typeof this.facade.load>) => this.facade.load(...args); endSession = (...args: Parameters<typeof this.facade.endSession>) => this.facade.endSession(...args); reset = (...args: Parameters<typeof this.facade.reset>) => this.facade.reset(...args); disposeChildren = (...args: Parameters<typeof this.facade.disposeChildren>) => this.facade.disposeChildren(...args); hydrate(snapshot: OrionplaceSnapshot | null | undefined) { if (!snapshot || snapshot === this.#snapshot) { return; } this.#snapshot = snapshot; if (snapshot.result && !snapshot.failure) { this.facade.applyResult(snapshot.result); } else { this.facade.failure = snapshot.failure ?? null; this.facade.status = snapshotStatus(snapshot); } } initialize() { return this.facade.load(requestBrowserAppLoad); } refresh() { return this.facade.load(requestBrowserAppLoad, true); } } export function setOrionplaceContext(snapshot?: OrionplaceSnapshot | null) { const store = new OrionplaceStore(snapshot); setContext(ORIONPLACE_KEY, store); return store; } export function getOrionplaceContext(): OrionplaceStore { const store = getContext<OrionplaceStore>(ORIONPLACE_KEY); if (!store) { throw new Error('The storefront layout must initialize Orionplace.'); } return store; }src/lib/stores/orionplace.svelte.tsThe store values:
init,profile,currencies,products,isAuthenticatedand more;useOrionplaceStore()returns it.Source code
import type { IApploadUserProfile } from '@/lib/interfaces/orionplace/appBase/IApploadUserProfile'; import { computed, ref, shallowRef, hasInjectionContext, inject, provide, } from '@/lib/orionplace/reactivity.svelte'; import type { InjectionKey, Ref } from '@/lib/orionplace/reactivity.svelte'; import type { StoreScope } from '@/lib/orionplace/scope.svelte'; import { defineStore, storeToRefs } from '@/lib/orionplace/scope.svelte'; import { useOrionplaceSelections } from '@/lib/stores/orionplaceSelections.svelte'; import { useOrionplaceBrowse } from '@/lib/stores/orionplaceBrowse.svelte'; import { useOrionplaceCatalog } from '@/lib/stores/orionplaceCatalog.svelte'; import { useOrionplaceProducts } from '@/lib/stores/orionplaceProducts.svelte'; import { attachOrionplaceDomainStores, registerOrionplaceDomainResolver, registerOrionplaceDomainStores, releaseOrionplaceDomainStores, } from '@/lib/stores/orionplaceDomainAccess.svelte'; import { guestAppLoadResult, guestProduct, } from '@/lib/utils/orionplace/guestSession'; import type { IProduct } from '@/lib/interfaces/orionplace/products/IProduct'; import type { AppLoadFailure, AppLoadResult, AppLoadLoader, AppLoadStatus, IAppLoadPathObject, } from '@/lib/interfaces/orionplace'; function forwardedState<T>(source: Ref<T>) { return computed({ get: () => source.value, set: (value: T) => { source.value = value; }, }); } /** State is scoped to the current SvelteKit render and hydrated from Application Load. */ export function createStoreState(scope: string, instanceScope: StoreScope) { const result = shallowRef<AppLoadResult | null>(null); const status = ref<AppLoadStatus>('idle'); const failure = shallowRef<AppLoadFailure | null>(null); const isReady = computed( () => status.value === 'success' && result.value !== null, ); const data = computed(() => result.value?.response.page_data ?? null); const init = computed(() => data.value?.init ?? null); const profile = computed(() => data.value?.user ?? null); const isAuthenticated = computed(() => init.value?.is_authenticated ?? false); const selections = useOrionplaceSelections(scope, instanceScope, init); const selectionRefs = storeToRefs(selections); const { locales, currencies, weights, lengths, currentLocale, currentCurrency, } = selectionRefs; const localeCode = forwardedState(selectionRefs.localeCode); const currencyId = forwardedState(selectionRefs.currencyId); const weightId = forwardedState(selectionRefs.weightId); const lengthId = forwardedState(selectionRefs.lengthId); const { setLocale, setCurrency, setUnit } = selections; const catalogState = useOrionplaceCatalog(scope, instanceScope, init); const catalogRefs = storeToRefs(catalogState); const { catalog, tariffs, storeTariffs, products, featured, catalogRoots, catalogShortcuts, catalogCategories, } = catalogRefs; const catalogSnapshot = forwardedState(catalogRefs.catalogSnapshot); const tariffsSnapshot = forwardedState(catalogRefs.tariffsSnapshot); const { setCatalogTree, setTariffs, setStoreTariffs, getTariff, getCatalogCategory, getCatalogProducts, } = catalogState; const productState = useOrionplaceProducts( scope, instanceScope, init, products, featured, ); const productRefs = storeToRefs(productState); const { homeProductSections, counters } = productRefs; const homeNewProductIds = forwardedState(productRefs.homeNewProductIds); const productSecondaryVariants = forwardedState( productRefs.productSecondaryVariants, ); const productUpdates = forwardedState(productRefs.productUpdates); const productRequests = forwardedState(productRefs.productRequests); const { setHomeNewProducts, getProducts, getProduct, setProduct, patchProductState, getProductRequest, setProductRequest, getSecondaryVariants, synchronizeProductCart, setSecondaryVariants, getProductAvailability, setProductCounters, } = productState; const browse = useOrionplaceBrowse(scope, instanceScope, init); const browseRefs = storeToRefs(browse); const searchQuery = forwardedState(browseRefs.searchQuery); const searchFacets = forwardedState(browseRefs.searchFacets); const searchFilters = forwardedState(browseRefs.searchFilters); const blogPosts = forwardedState(browseRefs.blogPosts); const { setSearchFacets, preserveSearchFacets, setSearchFilters, setBlogPosts, } = browse; registerOrionplaceDomainStores(instanceScope, scope, { selections, catalog: catalogState, products: productState, browse, }); function patchProfile(value: Partial<IApploadUserProfile>) { const current = result.value; const page = current?.response.page_data; if (!current || !page?.user || !isAuthenticated.value) { return; } result.value = { ...current, response: { ...current.response, page_data: { ...page, user: { ...page.user, ...value } }, }, }; } function patchWallet( wallet: NonNullable<NonNullable<typeof init.value>['wallet']>, ) { const current = result.value; const page = current?.response.page_data; if (!current || !page?.init || !isAuthenticated.value) { return; } result.value = { ...current, response: { ...current.response, page_data: { ...page, init: { ...page.init, wallet } }, }, }; } function applyResult(value: AppLoadResult) { catalogState.reset(); productState.reset(); browse.resetResults(); result.value = value; status.value = 'success'; failure.value = null; selections.reconcile(); } const publicEndpoints = computed(() => data.value?.unauth.paths ?? {}); const authenticatedEndpoints = computed(() => isAuthenticated.value ? (data.value?.auth?.paths ?? {}) : {}, ); const endpoints = computed<IAppLoadPathObject>(() => { const merged: IAppLoadPathObject = {}; for (const groups of [ publicEndpoints.value, authenticatedEndpoints.value, ]) { for (const [section, paths] of Object.entries(groups)) { merged[section] = { ...merged[section], ...paths }; } } return merged; }); const getEndpoint = (section: string, name: string) => endpoints.value[section]?.[name]; // 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; function reset() { generation++; controller?.abort(); controller = null; pending = null; result.value = null; catalogState.reset(); productState.reset(); browse.reset(); selections.reset(); failure.value = null; status.value = 'idle'; } /* * The shared API client calls this when a request answers HTTP 401 while * this store still shows a signed-in customer. The signed-in data is removed * at once, so the header and account pages show the guest state, and the * guest Application Load replaces the remaining snapshot in the background. * A guest store is left unchanged. */ function endSession(loader: AppLoadLoader): Promise<void> { if (!isAuthenticated.value || !result.value) { return pending ?? Promise.resolve(); } showGuestSnapshot(result.value); return load(loader, true, true); } function showGuestSnapshot(current: AppLoadResult) { const guestProducts = (items: Record<number, IProduct>) => Object.fromEntries( Object.entries(items).map(([id, item]) => [id, guestProduct(item)]), ); generation++; controller?.abort(); controller = null; pending = null; result.value = guestAppLoadResult(current); productState.clearSession(); productUpdates.value = { products: guestProducts(productUpdates.value.products), featured: guestProducts(productUpdates.value.featured), }; for (const state of Object.values(productSecondaryVariants.value)) { for (const variant of state.data?.variants ?? []) { variant.product = guestProduct(variant.product); } } } /* * `preserve` keeps the current snapshot visible while the replacement loads. * It is used only after `endSession` has already removed signed-in data. */ function load( loader: AppLoadLoader, force = false, preserve = false, ): Promise<void> { if (pending) { return pending; } // Reuse both SSR success and failure on hydration/navigation; retry is explicit. if (!force && status.value !== 'idle') { return Promise.resolve(); } const current = ++generation; controller = new AbortController(); const signal = controller.signal; if (!preserve) { catalogState.reset(); productState.reset(); browse.resetResults(); result.value = null; failure.value = null; status.value = 'pending'; } pending = Promise.resolve() .then(() => loader(signal)) .then((value) => { if (current !== generation) { return; } applyResult(value); }) .catch((error: unknown) => { if (current !== generation) { return; } const detail = error && typeof error === 'object' && 'data' in error ? error.data : null; const known = detail && typeof detail === 'object' && 'kind' in detail && ['http', 'validation', 'invalid-response', 'network'].includes( String(detail.kind), ); failure.value = known ? { kind: detail.kind as AppLoadFailure['kind'], httpStatus: 'httpStatus' in detail && typeof detail.httpStatus === 'number' ? detail.httpStatus : null, fields: 'fields' in detail && Array.isArray(detail.fields) ? detail.fields.filter( (field): field is string => typeof field === 'string', ) : [], } : { kind: 'network', httpStatus: null, fields: [] }; result.value = null; status.value = 'error'; }) .finally(() => { if (current !== generation) { return; } pending = null; controller = null; }); return pending; } function disposeChildren() { selections.$dispose(); catalogState.$dispose(); productState.$dispose(); browse.$dispose(); } return { result, status, failure, isReady, data, init, profile, patchProfile, patchWallet, isAuthenticated, locales, currencies, catalog, catalogSnapshot, setCatalogTree, catalogRoots, catalogShortcuts, catalogCategories, products, tariffs, tariffsSnapshot, setTariffs, storeTariffs, setStoreTariffs, featured, homeNewProductIds, homeProductSections, setHomeNewProducts, weights, lengths, localeCode, currencyId, weightId, lengthId, searchQuery, searchFacets, searchFilters, blogPosts, setSearchFacets, preserveSearchFacets, setSearchFilters, setBlogPosts, currentLocale, currentCurrency, setLocale, setCurrency, setUnit, getProducts, getTariff, getProduct, productUpdates, productRequests, getProductRequest, setProductRequest, counters, setProductCounters, setProduct, patchProductState, synchronizeProductCart, productSecondaryVariants, getSecondaryVariants, setSecondaryVariants, getProductAvailability, getCatalogCategory, getCatalogProducts, applyResult, publicEndpoints, authenticatedEndpoints, endpoints, getEndpoint, load, endSession, reset, disposeChildren, }; } export function createOrionplaceStore(id: string, scope: StoreScope) { const store = defineStore(id, () => createStoreState(id, scope))(scope); attachOrionplaceDomainStores(store, scope, id); return store; } import { getOrionplaceContext } from '@/lib/orionplace/store.svelte'; export function useOrionplaceStore(_scope?: StoreScope | null) { return getOrionplaceContext().facade; } registerOrionplaceDomainResolver(() => useOrionplaceStore());src/lib/stores/orionplaceSelections.svelte.tsThe visitor’s choices: language, currency, weight and length units.
Source code
import { computed, ref } from '@/lib/orionplace/reactivity.svelte'; import type { ComputedRef } from '@/lib/orionplace/reactivity.svelte'; import { defineStore } from '@/lib/orionplace/scope.svelte'; import type { StoreScope } from '@/lib/orionplace/scope.svelte'; import { resolveOrionplaceDomain } from '@/lib/stores/orionplaceDomainAccess.svelte'; import '@/lib/stores/orionplace.svelte'; import type { IAppLoadCommon } from '@/lib/interfaces/orionplace/appBase/IAppLoadCommon'; /** Customer choices shared by the storefront's language, currency and unit controls. */ export function useOrionplaceSelections( scope: string, instanceScope: StoreScope, init: ComputedRef<IAppLoadCommon | null>, ) { return defineStore(`${scope}:selections`, () => { const localeCode = ref(''); const currencyId = ref(''); const weightId = ref(''); const lengthId = ref(''); const locales = computed(() => init.value?.locale ?? {}); const currencies = computed(() => init.value?.currencies ?? {}); const weights = computed(() => init.value?.weights ?? {}); const lengths = computed(() => init.value?.lengths ?? {}); const currentLocale = computed(() => findLocale(localeCode.value)?.[1]); const currentCurrency = computed(() => currencies.value[currencyId.value]); function findLocale(code: string) { return Object.entries(locales.value).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(preferred?: number | string) { const id = String(preferred ?? ''); if (currencies.value[id]?.allowed_select) { return id; } return ( Object.keys(currencies.value).find( (key) => currencies.value[key]?.allowed_select, ) ?? '' ); } function setLocale(code: string) { const entry = findLocale(code); if (!entry) { return; } localeCode.value = entry[1].code?.trim() || entry[0]; currencyId.value = availableCurrency(entry[1].currency); weightId.value = unitId(weights.value, entry[1].weight); lengthId.value = unitId(lengths.value, entry[1].length); } function setCurrency(id: string) { if (currencies.value[id]?.allowed_select) { currencyId.value = id; } } function setUnit(kind: 'weights' | 'lengths', id: string) { const values = kind === 'weights' ? weights.value : lengths.value; if (!Object.hasOwn(values, id)) { return; } if (kind === 'weights') { weightId.value = id; } else { lengthId.value = id; } } function reconcile() { const selected = findLocale(localeCode.value) ?? Object.entries(locales.value)[0]; localeCode.value = selected?.[1].code?.trim() || selected?.[0] || ''; currencyId.value = availableCurrency( currencies.value[currencyId.value]?.allowed_select ? currencyId.value : selected?.[1].currency, ); weightId.value = unitId( weights.value, Object.hasOwn(weights.value, weightId.value) ? weightId.value : selected?.[1].weight, ); lengthId.value = unitId( lengths.value, Object.hasOwn(lengths.value, lengthId.value) ? lengthId.value : selected?.[1].length, ); } function reset() { localeCode.value = ''; currencyId.value = ''; weightId.value = ''; lengthId.value = ''; } return { localeCode, currencyId, weightId, lengthId, locales, currencies, weights, lengths, currentLocale, currentCurrency, setLocale, setCurrency, setUnit, reconcile, reset, }; })(instanceScope); } /** Access the selection store associated with the current storefront scope. */ export function useOrionplaceSelectionsStore() { return resolveOrionplaceDomain<ReturnType<typeof useOrionplaceSelections>>( 'selections', ); }src/lib/composables/useOrionplaceAppLoad.svelte.tsBrowser actions:
initialize,refresh,endSessionandreset.Source code
import { computed } from '@/lib/orionplace/reactivity.svelte'; import { storeToRefs } from '@/lib/orionplace/scope.svelte'; import { useOrionplaceStore } from '@/lib/stores/orionplace.svelte'; import { requestBrowserAppLoad } from '@/lib/orionplace/appLoadClient'; export function useOrionplaceAppLoad() { const store = useOrionplaceStore(); const refs = storeToRefs(store); const request = (signal: AbortSignal) => requestBrowserAppLoad(signal, store.localeCode); return { store, data: refs.result, status: refs.status, error: computed(() => (store.failure ? { data: store.failure } : null)), initialize: () => store.load(request), refresh: () => store.load(request, true), endSession: () => store.endSession(request), reset: () => store.reset(), }; }src/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 '@/lib/interfaces/orionplace/transport'; import { APP_ORIGIN, nativeAppRequest } from '@/lib/utils/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); }