Atomic Angular — Doc interne

Bootstrap the application

Sign the user in and initialize the application's stores — and, optionally, its dynamic routes — before the first view renders.

bootstrapApp() is the one call most applications register as an provideAppInitializer: it resolves the authentication mode, signs the user in, and initializes the application's stores in the right order, so no component has to guard against a half-initialized application.

Minimal example

Registered as an application initializer
app.config.ts
import { ApplicationConfig, provideAppInitializer } from "@angular/core";
import { bootstrapApp, provideAtomicClient } from "@sinequa/atomic-angular";

export const appConfig: ApplicationConfig = {
  providers: [
    provideAtomicClient({ app: "my-app" }),
    provideAppInitializer(() => bootstrapApp({ createRoutes: true })),
  ],
};

How it works

failed ok not authenticated authenticated failed ok provideAppInitializer client.initializeConfig() — resolves backendUrl + authMode navigate to /error wire client.config.auth.recoverFromUnauthorized to signIn() signIn() resolve(false) ApplicationService.initialize(createRoutes) resolve(true)

Resolving backendUrl and the authentication mode before signing in is what removes a long-standing bootstrap race: every store or service that derives its API URL from the client's configuration is only constructed after that resolution, so none of them can capture an unresolved backend URL. bootstrapApp also wires the client's recoverFromUnauthorized hook to signIn(), so a 401 on any of the client's own requests re-authenticates and replays the request once — the same recovery the HTTP interceptors provide for requests the client never sees (see Cross-cutting interceptors).

Once signed in, ApplicationService reads what every view needs — the app configuration, the principal and the user settings — in parallel: none depends on another's answer, only on the session, so they take one round trip rather than three. It then applies them to AppStore, PrincipalStore and UserSettingsStore in that order, resetting QueryParamsStore between the principal and the settings. A failed read is handled exactly as before: the configuration and the principal stop the start-up, the settings fall back to their defaults.

bootstrapApp keeps the client's uiLanguage on the interface language rather than a value fixed at start-up: it follows TranslocoService.langChanges$ for as long as the application runs, which is also what the request body interceptor and the syslang/translocoDate pipes read on their own side (see Cross-cutting pipes).

Recipes

Read the start-up data in one request

Without it, a signed-in start-up costs three requests sent together — GET api/v1/app, GET api/v1/principal, GET api/v1/user-settings. Behind a backend-for-frontend (BFF) that makes those calls itself, server-side, and answers them all at once — mint-dev-server's BFF answers GET /bff/bootstrap — provideStartupAggregation() replaces all three with a single request. Nothing else changes in the application: the stores are filled exactly as before.

Add it only when the application is actually served behind such an endpoint. Against a Sinequa server reached directly, it brings nothing and costs a round trip on every start-up: the request answers 404, a warning is logged, and only then do the parallel reads start. Those parallel reads are the default — they need no provider.

One request for configuration, principal and settings
app.config.ts
import { ApplicationConfig, provideAppInitializer } from "@angular/core";
import { bootstrapApp, provideAtomicClient, provideStartupAggregation } from "@sinequa/atomic-angular";

export const appConfig: ApplicationConfig = {
  providers: [
    provideAtomicClient({ app: "my-app" }),
    // Defaults to "bff/bootstrap" on the backend.
    provideStartupAggregation(),
    provideAppInitializer(() => bootstrapApp({ createRoutes: true })),
  ],
};

The endpoint answers with a StartupData: app, principal and userSettings, each one a StartupPart — { "ok": true, "value": … } when the BFF read it, { "ok": false, "status": … } when it could not. Any other key — mint-dev-server's BFF also sends userProfile — is ignored: the profile is read when the account settings panel opens, as without the provider. Here the settings call failed on the server:

GET bff/bootstrap → 200
{
  "app": { "ok": true, "value": { "name": "my-app", "queries": { "…": "…" } } },
  "principal": { "ok": true, "value": { "userId": "jdoe", "name": "Jane Doe", "…": "…" } },
  "userSettings": { "ok": false, "status": 503 }
}

With that answer, AppStore and PrincipalStore take their value without a request, and GET api/v1/user-settings is sent from the browser: every part the BFF could not read — or that its answer lacks — is fetched the usual way, all of them in parallel, and a failure of that read is handled as before. Only the browser's own reads can fail the start-up: the status or error a part reports is never treated as a failure.

If the request itself fails, or answers no data at all (an empty 204), the start-up does not stop: a warning is logged and it falls back to the three parallel reads — one round trip later. A failed request is also a failed request of the client: with provideAtomicNotifications(), a 400, 403, 500 or 503 shows an error toast, so a BFF that is down is visible to the user.

failed, or no data answered ok not read, or missing GET bff/bootstrap the three reads, in parallel each part store.hydrate(value) read from the browser, in parallel with the others

Use another endpoint path, or opt in per environment

The argument is the endpoint's path on the backend, resolved like every request of the client — relative to its backendUrl. Since the provider is an ordinary EnvironmentProviders, an application that runs both with and without a BFF can add it conditionally.

A custom path, only where a BFF is deployed
app.config.ts
import { ApplicationConfig, provideAppInitializer } from "@angular/core";
import { bootstrapApp, provideAtomicClient, provideStartupAggregation } from "@sinequa/atomic-angular";

import { environment } from "./environment";

export const appConfig: ApplicationConfig = {
  providers: [
    provideAtomicClient({ app: "my-app" }),
    // GET <backendUrl>/gateway/startup — and nothing at all where there is no BFF.
    ...(environment.hasBff ? [provideStartupAggregation("gateway/startup")] : []),
    provideAppInitializer(() => bootstrapApp({ createRoutes: true })),
  ],
};
environment.ts
export const environment = { hasBff: true };

A failing endpoint never breaks the start-up, but it delays it: the condition is what spares the backends without BFF a failed request and an extra round trip on every start-up.

Read the start-up data from somewhere else

provideStartupAggregation() is a shortcut for one STARTUP_DATA_SOURCE — a function returning a Promise<StartupData> that ApplicationService calls on every initialization: at start-up, and again after the sign-in form or a password change. Provide the token yourself to read the data some other way: here, from a JSON block the server inlined into index.html, which costs no request at all. The same rules apply: a part marked { ok: false }, or missing, is read the usual way, and a source that throws makes the start-up fall back to the parallel reads.

The inlined data describes the session the page was rendered for, so the source reads it once and removes it: after a sign-out and a sign-in — possibly as someone else — it throws, and the three parts are read again.

Start-up data inlined in the page by the server
app.config.ts
import { ApplicationConfig, provideAppInitializer } from "@angular/core";
import { bootstrapApp, provideAtomicClient, STARTUP_DATA_SOURCE, StartupData } from "@sinequa/atomic-angular";

// index.html, rendered by the server:
// <script id="startup-data" type="application/json">{ "app": { "ok": true, "value": … }, … }</script>
async function readInlinedStartupData(): Promise<StartupData> {
  const script = document.getElementById("startup-data");
  const json = script?.textContent;
  // Only valid for the first initialization: a later one may be another user's.
  script?.remove();
  // Throwing falls back to the three parallel reads.
  if (!json) throw new Error("No inlined start-up data");
  return JSON.parse(json) as StartupData;
}

export const appConfig: ApplicationConfig = {
  providers: [
    provideAtomicClient({ app: "my-app" }),
    { provide: STARTUP_DATA_SOURCE, useValue: readInlinedStartupData },
    provideAppInitializer(() => bootstrapApp({ createRoutes: true })),
  ],
};

A source does not have to provide every part. One that only knows the application configuration — cached from a previous visit, say — answers the others with { ok: false } (or leaves them out), and they are read the usual way, in parallel:

A source that only provides the configuration
app.config.ts
import { ApplicationConfig, provideAppInitializer } from "@angular/core";
import type { CCApp } from "@sinequa/atomic";
import { bootstrapApp, provideAtomicClient, STARTUP_DATA_SOURCE, StartupData } from "@sinequa/atomic-angular";

async function cachedAppOnly(): Promise<StartupData> {
  const cached = sessionStorage.getItem("my-app:config");
  return {
    app: cached ? { ok: true, value: JSON.parse(cached) as CCApp } : { ok: false },
    // Not read here: both are read the usual way, together.
    principal: { ok: false },
    userSettings: { ok: false },
  };
}

export const appConfig: ApplicationConfig = {
  providers: [
    provideAtomicClient({ app: "my-app" }),
    { provide: STARTUP_DATA_SOURCE, useValue: cachedAppOnly },
    provideAppInitializer(() => bootstrapApp({ createRoutes: true })),
  ],
};

Options

Prop

Type

provideStartupAggregation(endpoint?) takes the endpoint's path on the backend, "bff/bootstrap" by default. To read the start-up data some other way, provide STARTUP_DATA_SOURCE with a function returning a Promise<StartupData>.

The promise bootstrapApp returns never rejects — it resolves true once fully initialized, false when the user is not authenticated or an error occurred (already logged and, for a fatal error, already routed to /error).

Pitfalls

What's next

On this page