API Documentation

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.

POST/api/appload

What the response contains

page_data — the application data inside the API response. Its four main sections have different roles:

IAppLoad

Initial storefront data, customer profile and available API endpoints.

src/lib/interfaces/orionplace/appBase/IAppLoad.ts
TypeScript
import 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;
}

Session and cookies

The response creates the guest or customer session in the sessionid and csrftoken cookies. The gateway removes the API domain from them, so the cookies belong to your storefront. The server load passes these cookies to the browser with the very first page; after that every /api/... request sends them by itself.

What to build with init

Each field links to its data reference, actual consumers and related component guides.

API response field
page_data.init
POST/api/appload

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_data object of the response.
store.init
The initial data page_data.init; null until 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.
  1. src/hooks.server.ts

    Keeps personal pages out of caches and answers with the 503 error page when Application Load fails.

    Source code
    typescript
    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('<', '&lt;')
        .replaceAll('>', '&gt;');
    }
    
    /**
     * 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;
    };
    
  2. src/routes/api/[...orionplace]/+server.ts

    The gateway: every browser request to /api/... goes from here to your marketplace API with the storefront domain and the session cookies.

    Source code
    typescript
    // 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;
    
  3. src/lib/orionplace/appLoadServer.ts

    loadOrionplaceAppLoad() 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
    typescript
    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,
      };
    }
    
  4. src/routes/+layout.server.ts

    The root load runs Application Load before the page renders and returns the snapshot in the layout data.

    Source code
    typescript
    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,
      };
    };
    
  5. src/routes/+layout.svelte

    The root layout puts the snapshot into the store (hydrate) and shows the error page when the load failed.

    Source code
    svelte
    <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>
    
  6. src/lib/orionplace/store.svelte.ts

    One request’s store in the app context, so the data of different visitors never mix on the server.

    Source code
    typescript
    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;
    }
    
  7. src/lib/stores/orionplace.svelte.ts

    The store values: init, profile, currencies, products, isAuthenticated and more; useOrionplaceStore() returns it.

    Source code
    typescript
    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());
    
  8. src/lib/stores/orionplaceSelections.svelte.ts

    The visitor’s choices: language, currency, weight and length units.

    Source code
    typescript
    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',
      );
    }
    
  9. src/lib/composables/useOrionplaceAppLoad.svelte.ts

    Browser actions: initialize, refresh, endSession and reset.

    Source code
    typescript
    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(),
      };
    }
    
  10. src/lib/orionplace/appLoadClient.ts

    The browser request through the same-site gateway; the browser stores and sends the session cookies itself.

    Source code
    typescript
    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);
    }