# DcuplAppLoader API

The `DcuplAppLoader` handles fetching and processing configuration and data from dcupl Console or custom sources.

## Installation

```bash
npm install @dcupl/loader
```

## Basic usage

```typescript
import { Dcupl } from '@dcupl/core';
import { DcuplAppLoader } from '@dcupl/loader';

// projectId and apiKey belong to the dcupl instance, the loader reads them from there
const dcupl = new Dcupl({
  config: { projectId: 'my-project', apiKey: 'my-api-key' },
});
const loader = new DcuplAppLoader();

// Register the loader first: config.fetch() and process() need it
dcupl.loaders.add(loader, 'main');

// Fetch the loader configuration (dcupl.lc.json) from the dcupl CDN
await loader.config.fetch();

// Download the models and data listed in the configuration
await loader.process();

// Build models, references and indices
await dcupl.init();
```

The order matters: if the loader has not been added to a dcupl instance, `config.fetch()` throws and `process()` logs the error and resolves `false`. `dcupl.init()` does not process loaders. Call `loader.process()` before `dcupl.init()` (or before `dcupl.update()` when you load more data later).

`process()` without options loads every resource in the configuration. To load only the resources of one application, pass its key: see [Applications](#applications). For a step-by-step setup against a console project (project ID, Browser key, version, environments), see [Load from the console](/sdk/guides/load-from-console).

### Setup errors

| Call                                                                                              | Throws                                                                                                   |
| ------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `config.fetch()` before `dcupl.loaders.add()`                                                     | `dcupl not connected. Did you add your loader to dcupl? -> dcupl.loaders.add(loader)`                    |
| `config.fetch()` when the request fails or the response is not JSON                               | `Invalid Loader Configuration` (the cause is logged first)                                               |
| `process()`, `resources.plan()`, `resources.set()`, `add()` or `delete()` without a configuration | `There is no config present. Make sure to call loader.config.set(...) or loader.config.fetch(...) first` |
| `process({ applicationKey })` with an unknown key                                                 | `The applicationKey <key> does not exist`                                                                |

---

## config

Manage loader configuration.

### Methods

| Method  | Signature                                                                | Description                     |
| ------- | ------------------------------------------------------------------------ | ------------------------------- |
| `get`   | `() => ProjectConfiguration \| undefined`                                | Get current configuration       |
| `set`   | `(config: ProjectConfiguration) => void`                                 | Set configuration manually      |
| `fetch` | `(options?: FetchOptions) => Promise<ProjectConfiguration \| undefined>` | Fetch configuration from remote |

`get()` returns a copy and `set()` stores a copy, so changing the object afterwards does not change the loader's configuration.

### ProjectConfiguration

The shape of the loader configuration (`dcupl.lc.json`). [Loader configuration](/console/loader) explains tags, applications and environments.

```typescript
type ProjectConfiguration = {
  resources: Resource[]; // see Resource Types below
  applications?: {
    key: string;
    name?: string;
    description?: string;
    resourceTags?: (string | string[])[];
    variables?: Variable[];
  }[];
  environments?: {
    key: string;
    description?: string;
    variables?: Variable[];
    headers?: HttpConfig[]; // request headers for resource downloads
    queryParams?: HttpConfig[]; // query parameters appended to resource urls
  }[];
  options?: ProcessOptions['options']; // see ProcessOptions below
};

type Variable = { key: string; value: string | undefined; description?: string };

type HttpConfig = {
  key: string;
  value: string; // may contain ${variables}
  tags?: string[]; // only for resources sharing one of these tags; without tags: every resource (see below for headers)
  condition?: { applyIfVariableHasValue?: string }; // only when this variable has a non-empty value
};
```

An environment's variables, headers and query parameters apply only to a `process()` run that selects it with `environmentKeys`; an application preset does not select environments. Headers are only sent with resources that have `tags`: an untagged resource gets no environment headers, even from a header without `tags`.

### FetchOptions

```typescript
type FetchOptions = {
  baseUrl?: string; // Where the config file lives (default: your project's draft version on the dcupl CDN)
  loaderFileName?: string; // Config file name (default: 'dcupl.lc.json')
  headers?: { key: string; value: string }[]; // Request headers, used when fetching from a custom baseUrl
  skipApply?: boolean; // Return the config without applying it to the loader
};
```

There are no `projectId` or `apiKey` fetch options. Pass them to `new Dcupl({ config: { projectId, apiKey, version } })`: `dcupl.loaders.add()` copies them into the loader's `projectId`, `apiKey` and `version` variables (`version` defaults to `'draft'`), and the loader appends the API key to its requests to the dcupl CDN. A custom `baseUrl` also becomes the value of `${baseUrl}` in resource URLs, unless an environment, process or global variable `baseUrl` overrides it (the loader then logs a warning).

### Examples

```typescript
// Fetch from the dcupl CDN (projectId and apiKey come from new Dcupl({ config }))
await loader.config.fetch();

// Fetch from custom URL
await loader.config.fetch({
  baseUrl: 'https://my-cdn.example.com/config',
  loaderFileName: 'dcupl.loader.json',
  headers: [{ key: 'Authorization', value: 'Bearer my-token' }],
});

// Get current config
const config = loader.config.get();
console.log(config?.resources.length);

// Set config manually (models are loaded from 'model' resources too)
loader.config.set({
  resources: [
    { type: 'model', url: '/models/product.dcupl.json' },
    { key: 'products', type: 'data', url: '/data/products.json', model: 'Product' },
  ],
});
```

---

## resources

Manage data resources (models, data files, transformers, operators, scripts).

### Methods

| Method       | Signature                                    | Description                                                           |
| ------------ | -------------------------------------------- | --------------------------------------------------------------------- |
| `get`        | `(key: string) => Resource \| undefined`     | Get resource by key                                                   |
| `getAll`     | `() => Resource[]`                           | Get all resources                                                     |
| `getWithTag` | `(tag: string) => Resource[]`                | Get resources with specific tag                                       |
| `set`        | `(resources: Resource[]) => void`            | Replace all resources                                                 |
| `add`        | `(resource: Resource) => void`               | Add a resource                                                        |
| `delete`     | `(key: string) => void`                      | Delete a resource by key                                              |
| `plan`       | `(options?: ProcessOptions) => ResourcePlan` | Resolve what a `process(options)` call would load, without loading it |

`get` and `delete` match the optional `key` of a resource. `set`, `add` and `delete` need a configuration (fetched or set) first.

### Resource types

```typescript
type Resource = ModelResource | DataResource | TransformerResource | CustomOperatorResource | CustomScriptResource;

// Fields shared by every resource type
type ResourceBase = {
  key?: string; // Optional; used by resources.get()/delete() and as the plan id
  url: string; // May contain ${variables}
  type: 'model' | 'data' | 'transformer' | 'operator' | 'script';
  tags?: string[]; // For filtering resources and matching transformers
};

type ModelResource = ResourceBase & { type: 'model' }; // the file holds a model definition, an array of them, or a view definition

type DataResource = ResourceBase & {
  type: 'data';
  model: string; // Target model
  options?: {
    updateType?: 'set' | 'update' | 'upsert' | 'remove';
    keyProperty?: string | string[];
    keyPropertySeparator?: string;
    autoGenerateKey?: boolean;
    autoGenerateProperties?: boolean | { deep?: boolean; sampleSize?: number };
    csvParserOptions?: {
      delimiter?: string;
      quoteChar?: string;
      escapeChar?: string;
      commentChar?: string;
    };
  };
};

type TransformerResource = ResourceBase & {
  type: 'transformer';
  applyTo?: string[]; // Tags of the data resources it runs on
  transformerType?: 'rawFileTransformer' | 'parsedFileTransformer'; // Default: 'parsedFileTransformer'
};

type CustomOperatorResource = ResourceBase & {
  type: 'operator';
  operator: string; // The operator name usable in queries
};

type CustomScriptResource = ResourceBase & {
  type: 'script';
  scriptKey: string; // The key the script is registered under (see /sdk/guides/scripts)
};
```

Data resources default to `updateType: 'set'`: the resource's rows replace all rows of its model. When several data resources feed one model, the last one wins, so give the others `options: { updateType: 'upsert' }` (or pass `options: { defaultDataContainerUpdateType: 'upsert' }` to `process()`).

### Resource plan

`plan()` answers "what would `process(options)` load?" without fetching anything. It performs no
I/O and mutates no state, so it is safe to call repeatedly. Application preset, environments and
variables are resolved exactly as `process()` resolves them.

The plan is total: every entry in `config.resources` comes back either in `scheduled` or in
`skipped` with a reason — nothing is dropped silently. This is what lets you tell "loaded
everything" apart from "loaded everything this application was allowed to see".

```typescript
type ResourcePlan = {
  scheduled: ResourceItem[]; // what process() would load — tag-filtered, urls resolved
  skipped: SkippedResource[]; // declared but not scheduled for this run
  variables: Record<string, any>; // the variables the scheduled urls were resolved with
  run: RunContext; // the applicationKey / resourceTags / environmentKeys used
};

type SkippedResource = {
  resource: Resource;
  reason: 'no-tags' | 'tag-mismatch';
  id?: string;
};
```

Scheduled items are `Resource` plus:

| Field                 | Description                                                                                                                                                                                                                                                                                                                                                                         |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                  | Stable identity. An authored `resource.key` wins (`k:<key>`); otherwise a hash of the **raw** url plus type/model (`h:<hash>`). Derived from the raw url so config and runtime compute the same id — use it to join a runtime item back to its config entry instead of matching urls. Also available standalone as `resolveResourceId` / `resolveResourceIds` from `@dcupl/common`. |
| `rawUrl`              | The url as authored, before `${variable}` substitution (`url` holds the resolved value).                                                                                                                                                                                                                                                                                            |
| `unresolvedVariables` | Variables referenced by `rawUrl` that are not defined in any variable layer. Such a variable is substituted with an empty string, so a non-empty array here distinguishes a broken config from a missing file. A defined variable without a value (for example the built-in `projectId` when none is configured) also becomes an empty string but is not listed.                    |

The same information reaches progress callbacks: `ProgressItem` carries `skippedItems` and `run`
alongside the existing `totalItems` / `processedItems` / `downloadedItems`.

### Examples

```typescript
// Preview what a run would load, without loading it
const plan = loader.resources.plan({ applicationKey: 'catalog', resourceTags: ['critical'] });

console.log(plan.scheduled.map((r) => r.id));
plan.skipped.forEach((s) => console.log(`${s.resource.url} skipped: ${s.reason}`));

// Catch a misconfigured variable before it becomes a download failure
const broken = plan.scheduled.filter((r) => r.unresolvedVariables?.length);

// Get all resources
const resources = loader.resources.getAll();

// Get resources by tag
const criticalResources = loader.resources.getWithTag('critical');

// Add a new data resource
loader.resources.add({
  key: 'products-update',
  type: 'data',
  url: '/api/products/latest',
  model: 'Product',
  options: { updateType: 'upsert' },
  tags: ['live-data'],
});

// Delete a resource
loader.resources.delete('products-update');
```

---

## variables

Access configuration variables with environment support. Variables are referenced as `${name}` in resource URLs, headers and query parameters. When the same key is defined in several places, the later source wins: defaults < environment < process options < global. `config.fetch({ baseUrl, loaderFileName })` writes its options into the defaults, so an environment, process or global variable of the same name wins over them; when that happens with a different value, the loader logs a warning naming the variable, the fetch value and the winning layer (once per `process()` run).

The built-in default variables are `projectId`, `apiKey` and `version` (`'draft'`), filled from `new Dcupl({ config })`; `cdnBaseUrl` (`'https://cdn.dcupl.com/files/projects'`); `baseUrl` (`'${cdnBaseUrl}/${projectId}/${version}'`, or the `baseUrl` passed to `config.fetch()`); `modelUrl`, `dataUrl` and `transformerUrl` (`${baseUrl}/models`, `/data`, `/transformers`); `loaderFileName` (`'dcupl.lc.json'`); `cdnVersion`; and `consoleApiUrl`.

Variables inside variable values are substituted one level deep. With the default CDN `baseUrl` (`${cdnBaseUrl}/${projectId}/${version}`), `${baseUrl}` resolves fully, but `${modelUrl}`, `${dataUrl}` and `${transformerUrl}` keep placeholders, and `unresolvedVariables` does not flag them:

```text
${dataUrl}/products.csv  ->  ${cdnBaseUrl}/${projectId}/${version}/data/products.csv
${baseUrl}/data/products.csv  ->  https://cdn.dcupl.com/files/projects/<projectId>/draft/data/products.csv
```

Write resource URLs with `${baseUrl}/data/...` instead. When `baseUrl` is a literal URL (passed to `config.fetch({ baseUrl })` or set as a variable), `${modelUrl}`, `${dataUrl}` and `${transformerUrl}` resolve fully as well.

### Methods

| Method           | Signature                                              | Description                       |
| ---------------- | ------------------------------------------------------ | --------------------------------- |
| `get`            | `(key: string, env?: string) => Variable \| undefined` | Get variable by key               |
| `getAll`         | `(env?: string) => Variable[]`                         | Get all variables                 |
| `getAllAsObject` | `(env?: string) => Record<string, any>`                | Get variables as key-value object |

### Nested variable APIs

```typescript
// Built-in default variables (projectId, apiKey, version, baseUrl, ...)
loader.variables.default.get(key);
loader.variables.default.getAll();

// Environment variables (from config environments)
loader.variables.environment.get(env, key);
loader.variables.environment.getAll(env);

// Process variables (passed during processing)
loader.variables.process.get(key);
loader.variables.process.getAll();

// Global variables (set at runtime)
loader.variables.global.get(key);
loader.variables.global.getAll();
loader.variables.global.set(key, value);
loader.variables.global.delete(key);
```

### Examples

```typescript
// Get API URL variable, resolved for the production environment
const apiUrl = loader.variables.get('API_URL', 'production');
console.log(apiUrl?.value);

// Get all variables for production environment
const prodVars = loader.variables.getAll('production');

// Get variables as object for easy access
const vars = loader.variables.getAllAsObject('production');
console.log(vars.API_URL);

// Set global variable at runtime
loader.variables.global.set('AUTH_TOKEN', 'Bearer xyz');

// Use in resource URLs with interpolation
// URL: "${API_URL}/products" becomes "https://api.example.com/products"
```

---

## transformers

Transformers reshape a data resource's payload before dcupl ingests it. Declare them as `transformer` resources in the configuration: the `url` points to a JavaScript file whose body returns a `(resource, data) => data` function. `process()` loads them before the data and runs each one on every data resource whose `tags` share a tag with its `applyTo` (a transformer with an empty `applyTo` runs only on untagged data resources).

```typescript
loader.config.set({
  resources: [
    { type: 'model', url: '${baseUrl}/models/product.dcupl.json' },
    {
      type: 'transformer',
      url: '${baseUrl}/transformers/normalize-prices.js',
      applyTo: ['products'], // runs on data resources tagged 'products'
      transformerType: 'parsedFileTransformer', // default
    },
    { type: 'data', url: '${baseUrl}/data/products.csv', model: 'Product', tags: ['products'] },
  ],
});
```

```javascript filename="normalize-prices.js"
return (resource, items) =>
  items.map((item) => ({
    ...item,
    price: Number(item.price) || 0,
    currency: item.currency || 'USD',
  }));
```

A `parsedFileTransformer` receives the parsed items. A `rawFileTransformer` runs before parsing: it receives the CSV text of a CSV file, or the parsed body of a JSON file.

### Methods

| Method   | Signature                                                      | Description                                                        |
| -------- | -------------------------------------------------------------- | ------------------------------------------------------------------ |
| `get`    | `(options: { appyTo: string[] }) => TransformerConfigObject[]` | Registered transformers whose `applyTo` shares a tag with the list |
| `getAll` | `() => TransformerConfigObject[]`                              | All registered transformers                                        |
| `set`    | `(transformer: TransformerConfigObject) => void`               | Register a transformer (replaces one with the same key)            |
| `remove` | `(key: string) => void`                                        | Remove a transformer by key                                        |

> **Note:** The option of `transformers.get()` is spelled `appyTo`. `{ applyTo: [...] }` matches nothing.

### Transformer structure

```typescript
type TransformerConfigObject = {
  key: string; // A transformer loaded from a resource is registered under its resolved url
  applyTo: string[]; // Tags of the data resources it runs on
  type: 'rawFileTransformer' | 'parsedFileTransformer';
  transformer: (resource: AppLoaderConfiguration.Resource, input: any) => any;
};
```

`process()` only runs the transformers of the `transformer` resources in the current run. A transformer registered with `transformers.set()` is used when its `key` equals the resolved url of such a resource: the loader then uses your function instead of downloading the file. Under any other key it is stored but not applied.

### Examples

```typescript
// Supply the function for a declared transformer resource instead of downloading the file
loader.transformers.set({
  key: 'https://cdn.example.com/transformers/normalize-prices.js', // the resource's resolved url
  applyTo: ['products'],
  type: 'parsedFileTransformer',
  transformer: (resource, items) =>
    items.map((item: Record<string, any>) => ({ ...item, currency: item.currency || 'USD' })),
});

// Get registered transformers for a tag (note the spelling: appyTo)
const productTransformers = loader.transformers.get({
  appyTo: ['products'],
});

// Remove a transformer
loader.transformers.remove('https://cdn.example.com/transformers/normalize-prices.js');
```

### Errors

- A `parsedFileTransformer` that throws is recorded as a `TransformerError` loader error. The resource's data is still loaded, untransformed.
- A `rawFileTransformer` that throws fails the whole data resource (`InvalidResource`).
- With `new Dcupl({ scripting: { enabled: false } })` none of your scripts run: `transformer`, `operator` and `script` resources are skipped, each recorded as a `ScriptingDisabled` loader error and reported as a failed item in the progress.

Loader errors are recorded whether or not quality checks are enabled:

```typescript
const loaderErrors = dcupl.quality.errors.summary().filter((group) => group.type === 'loader');
// e.g. [{ key: 'Product::::TransformerError', model: 'Product', errorGroup: 'ResourceError', errorType: 'TransformerError', type: 'loader', totalCount: 1, ... }]
```

---

## Processing

Process the loaded configuration and apply to dcupl.

### process()

```typescript
await loader.process(options?: ProcessOptions); // Promise<boolean>
```

Downloads the scheduled resources and hands models and data to dcupl. Call it after `config.fetch()` or `config.set()` (it throws without a configuration) and before `dcupl.init()`. To load more resources later, call `process()` again and then `await dcupl.update()`. On a sealed instance (`performance.retainStagedData: false` or `dcupl.seal()`, see [seal()](/sdk/api/dcupl#seal)) `process()` rejects with `DcuplSealedError` before it fetches anything, so await it before `dcupl.init()`.

Running `process()` again on the same loader rebuilds the models it loads from their current files (changed types apply, removed attributes disappear, a model generated for a data file follows the current columns; properties are not duplicated), drops the resources that left the loader configuration since the last run (their views are unregistered, their models and rows removed, the rows of a removed data file gone), and leaves resources still in the configuration that the run does not select as they are. So the documented pattern of loading more with other `resourceTags` keeps working. After `await dcupl.update()` every existing list serves the new data. Data resources a run loads replace their model's rows again unless they use `updateType: 'upsert'`. A resource without a `key` is identified by its URL, so editing a literal URL counts as removing that resource and adding a new one; URLs built from `${baseUrl}` are not affected. Replacing the configuration (`config.set()` or `config.fetch()` with other resources) and processing again drops the resources the new configuration does not have.

> **Watch out:** One app loader per model key. When a loader's `process()` would load model files, view files or data for a key that another loader of the same instance already loads, it rejects with `SharedModelKeyError` (exported from `@dcupl/loader`, with `modelKey`, `owner` and `loader`) and changes nothing. Split models across loaders by key, not by file. `dcupl.loaders.remove(key)` frees the loader's keys: its models, views and rows go on the next `update()`, and models and views defined in code go back to their code definition.

### ProcessOptions

```typescript
type ProcessOptions = {
  applicationKey?: string; // Select an application preset (its resourceTags and variables), see Applications
  environmentKeys?: string[]; // Select environment configurations
  resourceTags?: (string | string[])[]; // Filter resources by tags
  variables?: { key: string; value: string | undefined; description?: string }[]; // Override variables
  options?: {
    defaultDataContainerUpdateType?: 'set' | 'update' | 'upsert' | 'remove';
    csvParserOptions?: CsvParserOptions;
    missingModelHandling?: {
      // default: true — a data resource whose model has no definition gets a model with inferred properties
      autoGenerateProperties?: boolean | { deep?: boolean; sampleSize?: number };
    };
  };
};
```

`resourceTags` entries are combined with OR; an entry that is itself an array is an AND group. `['critical', ['eu', 'live']]` loads resources tagged `critical` or tagged both `eu` and `live`. When you filter by tags, untagged resources are skipped.

The CSV parser reads `csvParserOptions` from the resource's `options` or from the configuration's top-level `options`. `csvParserOptions` passed to `process()` has no effect.

`options` passed to `process()` replace the configuration's top-level `options` for that run; they are not merged. Repeat any configured option you still need, for example `defaultDataContainerUpdateType`.

An `options.missingModelHandling` object replaces the default one as a whole, so `missingModelHandling: {}` turns missing-model handling off, the same as `autoGenerateProperties: false`: the rows of a data resource whose model isn't declared then load into no model, unless the resource's own `options.autoGenerateProperties` is `true`.

### Examples

```typescript
// Process with specific environment
await loader.process({
  environmentKeys: ['production'],
});

// Process only tagged resources
await loader.process({
  resourceTags: ['critical', 'models'],
});

// Process with variable overrides
await loader.process({
  variables: [{ key: 'API_URL', value: 'https://staging-api.example.com' }],
});
```

### CSV files

A data resource is parsed as CSV when its resolved URL ends with `.csv` or the response's `content-type` contains `text/csv`. Any other response is parsed as JSON. The cells are converted against the target model:

- The delimiter is detected from the first line (`,`, `;`, tab or `|`) unless `csvParserOptions.delimiter` is set.
- Key columns are kept as written.
- In every other column, `null` becomes `null` and an empty cell becomes `undefined`.
- `int` and `float` use `parseInt` / `parseFloat`, so `12abc` becomes `12`. `boolean` accepts `true`, `false`, `1` and `0` in any case (an empty cell is no value, anything else is a `WrongDataType`). `json` cells are parsed with `JSON.parse`.
- `Array<...>` properties and `multiValued` references are split on the attribute's `separator` (default `,`), or parsed as JSON when the cell starts with `[`.
- A column the model doesn't declare is dropped and reported once as an `UnknownColumn` error, unless the model has `autoGenerateProperties`.
- A cell that can't be converted is dropped and reported as `WrongDataType`. Parsing never throws.

With `autoGenerateProperties` (also on a model the loader generates for a data resource without one), undeclared columns arrive as strings and are converted by the inferred type (`TRUE` / `False` columns infer as `boolean`, `1` / `0` columns stay `int`; see [autoGenerateProperties](/sdk/api/models#autogenerateproperties)).

---

## Applications

An application is a named preset in the configuration's `applications[]`: a `key`, an optional `name` and `description`, the `resourceTags` it loads and the `variables` for its runs. `process({ applicationKey })` loads the resources of that application; `resources.plan({ applicationKey })` shows which ones they are.

```json filename="dcupl.lc.json"
{
  "applications": [
    { "key": "catalog", "resourceTags": ["catalog"] },
    { "key": "default", "name": "Default" }
  ],
  "resources": [
    { "type": "model", "url": "${baseUrl}/models/product.dcupl.json", "tags": ["catalog"] },
    { "type": "data", "url": "${baseUrl}/data/products.csv", "model": "Product", "tags": ["catalog"] },
    { "type": "model", "url": "${baseUrl}/models/order.dcupl.json", "tags": ["orders"] },
    { "type": "data", "url": "${baseUrl}/data/orders.csv", "model": "Order", "tags": ["orders"] }
  ]
}
```

```typescript
await loader.process({ applicationKey: 'catalog' }); // Product only
await loader.process({ applicationKey: 'default' }); // no resourceTags: every resource
await loader.process(); // no application: every resource
```

- Without `applicationKey`, `process()` loads every resource (narrowed only by `resourceTags` you pass).
- An application without `resourceTags` loads every resource. With tags, untagged resources are skipped.
- `resourceTags` or `variables` passed to `process()` replace the application's own.
- An unknown key throws `The applicationKey <key> does not exist`, also when the configuration has no `applications` at all.
- Projects from `dcupl init` and the console's starter projects define `default` (without tags, so it loads every resource). A configuration you write yourself may have none, and then `process({ applicationKey: 'default' })` throws. Pass a key only when your configuration defines that application.

An application does not select environments: pass `environmentKeys` for that. [Loader configuration](/console/loader#tags-and-applications) shows how tags and applications fit together in `dcupl.lc.json`.

---

## Progress tracking

Subscribe to loader progress events.

### on()

```typescript
import type { AppLoaderProgress } from '@dcupl/loader';

const unsubscribe = loader.on((progress: AppLoaderProgress.ProgressItem) => {
  console.log(`${progress.processed}/${progress.total} resources processed`);
});

// Later: unsubscribe
unsubscribe();
```

### ProgressItem

The callback receives a snapshot of the whole run, not a single event — every list below is the
current state, re-sent on each update.

```typescript
type ProgressItem = {
  total: number; // resources scheduled for this run
  processed: number;
  remaining: number; // 0 once the run is finished
  progress: number; // percentage, 0-100
  context: string; // the type of the resource just handled: 'model', 'data', 'transformer', ...
  currentItem: ResourceItem; // the resource this update is about (undefined in the first update)

  totalItems: ResourceItem[]; // scheduled — tag-filtered, urls resolved
  processedItems: ResourceItem[];
  downloadedItems: ResourceItem[];
  failedItems: ResourceItem[];
  remainingItems: ResourceItem[];

  skippedItems?: SkippedResource[]; // declared but never scheduled, with a reason
  run?: RunContext; // applicationKey / resourceTags / environmentKeys
};
```

The first update is sent when the run starts, before any resource is handled, so guard
`currentItem` with `?.`. A resource's outcome lives on the item, not on the event: `success` is a
boolean and `status` is the HTTP status code. There is no completion event — a run is done when
`remaining === 0`.

```typescript
loader.on((progress) => {
  if (progress.remaining !== 0) return;

  for (const failure of progress.failedItems) {
    // status is only set when a response arrived (not on a network error or a bad url)
    console.error(`${failure.key ?? failure.url} failed with ${failure.status ?? 'no response'}`);
  }
});
```

---

## Complete example

```typescript
import { Dcupl } from '@dcupl/core';
import { DcuplAppLoader } from '@dcupl/loader';

// Runs in the browser (navigator.language below)
async function initializeDcupl() {
  const dcupl = new Dcupl({
    config: {
      projectId: 'my-project',
      apiKey: 'my-read-only-api-key', // a scoped read-only key: it ships to every visitor
    },
  });
  const loader = new DcuplAppLoader();

  // Register with dcupl before fetching
  dcupl.loaders.add(loader, 'main');

  // Track progress
  loader.on((progress) => {
    const current = progress.currentItem?.key ?? progress.currentItem?.url;
    console.log(`Loading: ${current ?? 'starting'} (${progress.processed}/${progress.total})`);
  });

  // Fetch configuration
  await loader.config.fetch();

  // Set runtime variables, usable as ${USER_LOCALE} in resource urls
  loader.variables.global.set('USER_LOCALE', navigator.language);

  // Download models and data with the production environment
  await loader.process({
    environmentKeys: ['production'],
  });

  // Initialize
  await dcupl.init();

  return dcupl;
}
```

---

## See also

- [Setup options](/sdk/setup-options) - Choosing between SDK-only and Loader approaches
- [Loader configuration](/console/loader) - The `dcupl.lc.json` file: resources, tags, applications, environments
- [Dcupl API](/sdk/api/dcupl) - Main dcupl instance API
- [Data management](/sdk/core-concepts/data-management) - Working with data
