Документация API

Инициализация приложения

Загрузка приложения · Svelte

Загрузка приложения — первый запрос магазина к персональному бэкенду. Он возвращает настройки и начальный контент для интерфейса, профиль вошедшего покупателя и методы API, доступные текущей сессии.

POST/api/appload

Что приходит в ответе

page_data — данные приложения внутри ответа API. У четырёх основных разделов разные задачи:

IAppLoad

Начальные данные магазина, профиль покупателя и доступные эндпоинты API.

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 {
	// Настройки и начальные данные магазина.
	init: IAppLoadCommon;
	// Публичные методы API, сгруппированные в paths.
	unauth: IAppLoadPathsRecord;
	// Профиль текущего покупателя. Отсутствует у гостя.
	user?: IApploadUserProfile;
	// Методы авторизованной сессии. Отсутствуют у гостя; права проверяет бэкенд.
	auth?: IAppLoadPathsRecord;
	// Показывает, включены ли функции панели продавца для этого магазина.
	seller_panel_enabled?: boolean;
	// Служебный ключ предпросмотра товаров для вошедшего аккаунта с данными
	// продавца.
	// Не публикуйте значение; покупательскому интерфейсу этот ключ не нужен.
	view_key?: string;
	// Время подготовки загрузки приложения на бэкенде, в секундах; только при
	// debug
	// проекта. Не включает сериализацию ответа и передачу по сети.
	spent_total?: number;
	// Время загрузки init на бэкенде, в секундах. Только при debug проекта.
	spent_init?: number;
	// Время загрузки публичных путей API, в секундах. Только при debug проекта.
	unauth_spent?: number;
	// Время подготовки профиля и auth, в секундах. Только при debug проекта; 0 у
	// гостя.
	auth_spent?: number;
}

Сессия и cookie

Ответ создаёт сессию гостя или покупателя в cookie sessionid и csrftoken. Шлюз убирает из них домен API, поэтому cookie принадлежат вашей витрине. Серверный load передаёт эти cookie браузеру уже с первой страницей; дальше все запросы к /api/... отправляют их сами.

Для чего использовать поля init

Каждое поле ведёт к описанию данных, их использования и связанных компонентов.

Поле ответа API
page_data.init
POST/api/appload
Доступ к хранилищуuseOrionplaceStore()
store.initsrc/lib/stores/orionplace.svelte.ts
  • Язык, валюты и единицы

    Языки, валюты и единицы измерения с названиями, форматами отображения и значениями по умолчанию для каждой локали.

  • Каталог и поиск

    Категории, начальные коллекции товаров, поисковые подсказки и тарифы подписки.

  • Оформление и метаданные

    Логотипы, значок сайта, настройки отображения магазина и SEO-метаданные.

    • logosstore.init?.logos

      BrandLogos показывает логотип выбранного размера или значок.

    • visual_settingsstore.init?.visual_settings

      Изображения в меню категорий, количество и порядок категорий главной, остальные настройки отображения магазина.

    • seostore.init?.seo

      getStorefrontSeo() подготавливает метаданные для app/pages/index.vue: заголовок, описание и ключевые слова применяются через useSeoMeta(), дополнительные теги — через useHead().

  • Контент и контакты

    Баннеры, события магазина, контакты компании и ссылки на социальные сети.

  • Покупатель и счётчики

    Признак авторизации, баланс кошелька и начальные счётчики корзины и избранного. Полная корзина и данные личного кабинета доступны через отдельные методы API.

  • Карты и интеграции

    Центр карты, публичный ключ карты и доступность поиска адреса по координатам.

Состав полей зависит от настроек магазина и текущей сессии. Поле может отсутствовать, а список — быть пустым. Начальная подборка товаров не заменяет полный каталог с пагинацией.

Как данные попадают в компоненты

useOrionplaceStore()
Store текущего запроса из контекста приложения; вызывается при инициализации компонента.
store.result
Весь результат запроса: HTTP-статус и ответ сервера.
store.data
Объект page_data из ответа.
store.init
Начальные данные page_data.init; пока загрузки не было, null.
store.profile
Профиль покупателя после входа, иначе null.
store.status · store.isReady · store.failure
Состояние загрузки: идёт, готово или ошибка с её видом и полями.
  1. src/hooks.server.ts

    Запрещает кэшировать персональные страницы и отдаёт страницу ошибки 503, если Application Load не удался.

    Исходный код
    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

    Шлюз: каждый запрос браузера к /api/... уходит отсюда в ваш API маркетплейса с доменом витрины и cookie сессии.

    Исходный код
    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() вызывает тот же шлюз на сервере и передаёт браузеру cookie сессии из ответа. Ошибка не бросается, а попадает в snapshot.

    Исходный код
    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

    Корневой load выполняет Application Load до рендера страницы и возвращает snapshot в данных layout.

    Исходный код
    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

    Корневой layout кладёт snapshot в store (hydrate) и показывает страницу ошибки, если загрузка не удалась.

    Исходный код
    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

    Store одного запроса в контексте приложения, поэтому данные разных посетителей на сервере не смешиваются.

    Исходный код
    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

    Значения store: init, profile, currencies, products, isAuthenticated и другие; useOrionplaceStore() возвращает его.

    Исходный код
    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

    Выбор посетителя: язык, валюта, единицы веса и длины.

    Исходный код
    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

    Действия в браузере: initialize, refresh, endSession и reset.

    Исходный код
    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

    Запрос из браузера через шлюз того же сайта; cookie сессии браузер хранит и отправляет сам.

    Исходный код
    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);
    }