# Lists API

The Lists API (`dcupl.lists`) provides methods for creating and managing isolated data collections with their own filters, queries, and state management.

## Overview

Lists are isolated views of your data that maintain their own query state and provide access to the catalog API for querying, filtering, and analytics. Each list operates independently and can have different filters applied to the same underlying data model.

## Methods

### create()

Creates a new list instance.

**Signature:**

```typescript
create<T extends RawItem = RawItem>(options: {
  modelKey: string;
  listKey?: string;
  listItemKeys?: string[];
  query?: DcuplGlobalQueryOptions<ListItem>;
}): DcuplList<T>
```

**Parameters:**

| Parameter | Type                                    | Required | Description                |
| --------- | --------------------------------------- | -------- | -------------------------- |
| `options` | [`DcuplListOptions`](#dcupllistoptions) | Yes      | List configuration options |

**Returns:** `DcuplList<T>` - The created list instance

`create<Product>()` returns a `DcuplList<Product>` (SDK 2.0): `items()`, `first()`, `execute()`, `any()` and `count()` keep that item type whatever options you pass (`items({ sort })` no longer narrows it to the sorted fields). A type argument on a read must extend the list's item type: `items<ProductWithStock>()` adds fields, it cannot drop them (`items<Pick<Product, 'key' | 'name'>>()` is a type error). For a projected shape, create the list without a type argument (or with `create<Partial<Product>>()`) and type the read, `items<{ key: string; name: string }>({ projection: { name: true } })`, or cast the result. `one()` and `many()` take no type argument and return the list's item type. The published list query types use `NoInfer`, so consumers need TypeScript 5.4 or later.

**Throws:** `Model with key <modelKey> not found` when the model does not exist, which includes calling
`create()` before `dcupl.init()` has run. An existing `listKey` is not reused: `create()` adds a second
list with the same key.

A list created with `listItemKeys` or `query` is **scoped**: it keeps a set of keys (the
`listItemKeys` you pass, or the keys the `query` matches when the list is created), and its own
catalog query starts empty (`clear()` does not remove the scope). It is not an initial filter. The
list always serves the model's current items for those keys: after `dcupl.update()` changed values
show, removed items drop out, and an item added under one of the keys is served. For a
`listItemKeys` list that is all there is, so [auto-update](#auto-update-pattern) changes only
the events, not the items. Items that start matching a seed `query` join only when you call [`update()`](#update)
or enable auto-update.

**Examples:**

```typescript title="Create list for all products"
const productList = dcupl.lists.create({
  modelKey: 'Product',
});
```

```typescript title="Create list with custom key"
const activeProducts = dcupl.lists.create({
  modelKey: 'Product',
  listKey: 'activeProducts',
});
```

```typescript title="Create a list scoped to specific items"
const selectedProducts = dcupl.lists.create({
  modelKey: 'Product',
  listItemKeys: ['p1', 'p2', 'p3'],
});
```

```typescript title="Create a list scoped to a query"
const expensiveProducts = dcupl.lists.create({
  modelKey: 'Product',
  query: {
    modelKey: 'Product',
    queries: [{ operator: 'gte', attribute: 'price', value: 100 }],
  },
});
```

```typescript title="Create a scoped list with auto-update enabled"
import type { DcuplListOptions } from '@dcupl/common';

// autoUpdate is missing from the create() parameter type,
// so pass the options as a DcuplListOptions variable
const options: DcuplListOptions = {
  modelKey: 'Product',
  query: {
    modelKey: 'Product',
    queries: [{ operator: 'gte', attribute: 'price', value: 100 }],
  },
  autoUpdate: true,
};
const list = dcupl.lists.create(options);
```

### get()

Retrieves a list by its key.

**Signature:**

```typescript
get(listKey: string): DcuplList | undefined
```

**Parameters:**

| Parameter | Type     | Required | Description                     |
| --------- | -------- | -------- | ------------------------------- |
| `listKey` | `string` | Yes      | The key of the list to retrieve |

**Returns:** `DcuplList | undefined` - The list instance or undefined if not found

**Examples:**

```typescript title="Get an existing list"
const list = dcupl.lists.get('activeProducts');
if (list) {
  const items = list.catalog.query.items();
}
```

```typescript title="Check if list exists"
const exists = dcupl.lists.get('myList') !== undefined;
```

```typescript title="Safe access"
const list = dcupl.lists.get('myList');
if (list) {
  // Use list
} else {
  // Create list
  const newList = dcupl.lists.create({
    modelKey: 'Product',
    listKey: 'myList',
  });
}
```

### getAll()

Retrieves all lists or filters by model.

**Signature:**

```typescript
getAll(filter?: { modelKey: string }): DcuplList[]
```

**Parameters:**

| Parameter | Type                   | Required | Description                  |
| --------- | ---------------------- | -------- | ---------------------------- |
| `filter`  | `{ modelKey: string }` | No       | Optional filter by model key |

**Returns:** `DcuplList[]` - Array of list instances

The result also contains the internal `__global_list__<modelKey>` lists that `dcupl.fn.*`,
`dcupl.query.execute()` and `create()` with a `query` set up. Skip keys starting with
`__global_list__` when you only want your own lists.

**Examples:**

```typescript title="Get all lists"
const allLists = dcupl.lists.getAll();
console.log(allLists.length);
```

```typescript title="Get lists for specific model"
const productLists = dcupl.lists.getAll({ modelKey: 'Product' });
```

```typescript title="Update all lists"
dcupl.lists.getAll().forEach((list) => {
  list.update();
});
```

```typescript title="Count lists per model"
const listsByModel: Record<string, number> = {};
dcupl.lists.getAll().forEach((list) => {
  listsByModel[list.modelKey] = (listsByModel[list.modelKey] || 0) + 1;
});
```

### destroy()

Destroys lists and cleans up resources.

**Signature:**

```typescript
destroy(options?: { listKey?: string; modelKey?: string }): void
```

**Parameters:**

| Parameter          | Type     | Required | Description                            |
| ------------------ | -------- | -------- | -------------------------------------- |
| `options`          | `object` | No       | Destroy options (all lists if omitted) |
| `options.listKey`  | `string` | No       | Specific list key to destroy           |
| `options.modelKey` | `string` | No       | Destroy all lists for a model          |

**Examples:**

```typescript title="Destroy specific list"
dcupl.lists.destroy({ listKey: 'activeProducts' });
```

```typescript title="Destroy all lists for a model"
dcupl.lists.destroy({ modelKey: 'Product' });
```

```typescript title="Destroy all lists"
dcupl.lists.destroy();
```

```typescript title="Safe destroy with check"
const list = dcupl.lists.get('myList');
if (list) {
  dcupl.lists.destroy({ listKey: 'myList' });
}
```

## DcuplList class

The `DcuplList` class represents an individual list instance with its own state and catalog API.

### Properties

#### key

The unique identifier for this list.

**Type:** `string`

**Example:**

```typescript
const list = dcupl.lists.create({
  modelKey: 'Product',
  listKey: 'myList',
});
console.log(list.key); // 'myList'
```

#### modelKey

The model this list is based on.

**Type:** `string`

**Example:**

```typescript
console.log(list.modelKey); // 'Product'
```

#### catalog

Access to the catalog API for querying and analytics.

**Type:** `DcuplList<T>['catalog']`, an instance of the `Catalog` class typed with the list's item type. `Catalog` is not exported from the `@dcupl/core` main entry; import it from `@dcupl/core/catalog` if you need the class itself.

**See:** [Catalog API Reference](/sdk/api/catalog)

**Example:**

```typescript
const items = list.catalog.query.items();
const facets = list.catalog.fn.facets({ attribute: 'category' });
```

### Methods

#### update()

Re-derives the list from the current data: a `query`-scoped list re-runs its seed `query` (so new
matches join and items that stop matching leave), and the list's caches are rebuilt. A
`listItemKeys` list re-applies its keys, which gives the items it already serves after
`dcupl.update()`. Emits `list_updated` (not `result_updated`).

**Signature:**

```typescript
update(options?: { reprocessData?: boolean }): void
```

**Parameters:**

| Parameter               | Type      | Required | Description                                                                                                                                    |
| ----------------------- | --------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `options`               | `object`  | No       | Update options                                                                                                                                 |
| `options.reprocessData` | `boolean` | No       | `update()` and `update({ reprocessData: true })` re-derive the list; an options object without it skips that (`list_updated` is still emitted) |

**Examples:**

```typescript title="Basic update"
list.update();
```

```typescript title="Re-derive a query-scoped list after a data change"
dcupl.data.update([{ key: 'p1', price: 150 }], { model: 'Product' });
await dcupl.update();
list.update(); // Only needed to re-run the seed query of a query-scoped list
```

> **Note:** `await dcupl.update()` already drops the query caches of every affected list, so
> an unscoped list returns the new data without any extra call, and so does a `listItemKeys`
> list. Use `update()` when a list was created with a `query` scope and you want that query
> re-run on the current data.

> **Note:** A scoped list that does not auto-update keeps its key set but serves current values:
> after a partial update as well as after a full rebuild (`data.set()`, `data.reset()`, or an update
> above the [partial-update thresholds](/sdk/api/dcupl#dcuplpartialupdateconfig)) its queries and
> functions run on the model's current items, and removed items are gone. Only new items that would
> match the seed query need `list.update()` or `autoUpdate: true` to join; a `listItemKeys` list
> also serves an item added later under one of its keys.

```typescript title="Auto-update workflow"
const list = dcupl.lists.create(options); // a query-scoped list with autoUpdate: true (see create())
dcupl.data.upsert([{ key: 'p9', name: 'New Product', price: 120 }], { model: 'Product' });
await dcupl.update(); // re-derives the list: p9 joins it without a list.update() call
```

#### invalidate()

Drops the list's query and result caches. The filtered data is recomputed lazily
on the next read, from the list's current data — without re-deriving a scoped
list's item set (that is what [`update()`](#update) does).

`dcupl.update()` calls this automatically for every affected list that does not
auto-update, so a query executed before the update can never keep serving its
pre-update result. Call it yourself only when you mutate list data outside the
normal `dcupl.update()` flow. It keeps a scoped list's key set, so it does not replace
`update()` when new matches of a seed query should join the list.

**Signature:**

```typescript
invalidate(): void
```

**Examples:**

```typescript title="Drop cached query results"
list.invalidate();
list.catalog.query.execute(); // recomputed from current data
```

#### destroy()

Destroys the list and cleans up resources.

**Signature:**

```typescript
destroy(): void
```

**Examples:**

```typescript title="Destroy a list"
const list = dcupl.lists.create({ modelKey: 'Product' });
list.destroy();
```

```typescript title="Cleanup in component"
import type { DcuplList } from '@dcupl/core';

class MyComponent {
  private list: DcuplList;

  constructor() {
    this.list = dcupl.lists.create({ modelKey: 'Product' });
  }

  destroy() {
    this.list.destroy();
  }
}
```

#### on()

Subscribes to every event of the list, including reads (`query_execute`, `fn_facets`, …), plus the
instance's `dcupl_updated_manually` after each `dcupl.update()`. To
re-render when the list's result changes, use [`onResultChange()`](#onresultchange). See the
[Events reference](/sdk/api/events) for all event types.

**Signature:**

```typescript
on(callback: CallbackFunction): () => void
```

**Parameters:**

| Parameter  | Type               | Required | Description                 |
| ---------- | ------------------ | -------- | --------------------------- |
| `callback` | `CallbackFunction` | Yes      | Function to call on changes |

**Returns:** `() => void` - Unsubscribe function

**Examples:**

```typescript title="Subscribe to list changes"
const unsubscribe = list.on((message) => {
  console.log('List changed:', message);
});
```

```typescript title="Unsubscribe"
unsubscribe();
```

```typescript title="React to specific events"
list.on((message) => {
  if (message.type === 'result_updated') {
    console.log('Result changed');
  }
  if (message.type === 'query_apply') {
    console.log('Query was applied');
  }
});
```

```typescript title="Multiple subscribers"
const unsub1 = list.on(callback1);
const unsub2 = list.on(callback2);

// Cleanup
unsub1();
unsub2();
```

#### onResultChange()

Subscribes to changes of the list's result: `list.on()` filtered to `result_updated`. The callback
fires when a query change recomputes the result (`addCondition`, `removeGroup`, `clear`,
`applyOptions`, …; not with `{ skipProcessing: true }`) and once per `dcupl.update()` that can have changed the list. It never fires
for reads (`items()`, `execute()`, `fn.facets()`, …) or for a manual `list.update()`. This is the
recommended hook for re-rendering.

**Signature:**

```typescript
onResultChange(callback: CallbackFunction): () => void
```

**Returns:** `() => void` - Unsubscribe function

**Examples:**

```typescript title="Re-render when the result changes"
const unsubscribe = list.onResultChange(() => {
  renderProducts(list.catalog.query.items());
});

list.catalog.query.addCondition({ operator: 'eq', attribute: 'status', value: 'active' }); // fires once

dcupl.data.upsert([{ key: 'p5', name: 'Chair', price: 80, status: 'active' }], { model: 'Product' });
await dcupl.update(); // fires once

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

## Configuration types

### DcuplListOptions

Configuration options for creating a list.

**Type Definition:**

```typescript
type DcuplListOptions = {
  modelKey: string;
  listKey?: string;
  listItemKeys?: string[];
  query?: DcuplGlobalQueryOptions<ListItem>;
  autoUpdate?: boolean;
};
```

**Properties:**

| Property       | Type                                | Required | Description                                                                                                                                                                                                                                                                                      |
| -------------- | ----------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `modelKey`     | `string`                            | Yes      | Model this list is based on                                                                                                                                                                                                                                                                      |
| `listKey`      | `string`                            | No       | Unique list identifier (auto-generated if omitted)                                                                                                                                                                                                                                               |
| `listItemKeys` | `string[]`                          | No       | Scopes the list to these item keys (the key set is kept across updates; current values are served, and an item added later under one of the keys joins)                                                                                                                                          |
| `query`        | `DcuplGlobalQueryOptions<ListItem>` | No       | Seed query: scopes the list to the items matching it at creation (key set kept across updates, not an initial filter)                                                                                                                                                                            |
| `autoUpdate`   | `boolean`                           | No       | Re-run a seed `query` on every `dcupl.update()` (new matches join, items that stop matching leave); a `listItemKeys` list serves the same items either way, with it the list also emits `list_updated` on every update that affects it. Default: `performance.autoUpdateLists.enabled` (`false`) |

> **Watch out:** `autoUpdate` works at runtime but is missing from the `dcupl.lists.create()` parameter type, so
> TypeScript rejects it in an object literal passed to `create()`. Declare the
> options as `DcuplListOptions` (from `@dcupl/common`) and pass the variable, as in the examples below.

**Examples:**

```typescript title="Minimal configuration"
const options: DcuplListOptions = { modelKey: 'Product' };
```

```typescript title="With custom key"
const options: DcuplListOptions = {
  modelKey: 'Product',
  listKey: 'featuredProducts',
};
```

```typescript title="Scoped to specific items"
const options: DcuplListOptions = {
  modelKey: 'Product',
  listItemKeys: ['p1', 'p2', 'p3'],
};
```

```typescript title="Scoped to a query"
const options: DcuplListOptions = {
  modelKey: 'Product',
  query: {
    modelKey: 'Product',
    queries: [{ operator: 'eq', attribute: 'status', value: 'active' }],
  },
};
```

```typescript title="Scoped to a query, with auto-update"
const options: DcuplListOptions = {
  modelKey: 'Product',
  listKey: 'activeProducts',
  query: {
    modelKey: 'Product',
    queries: [{ operator: 'eq', attribute: 'status', value: 'active' }],
  },
  autoUpdate: true,
};
```

### Auto-update pattern

Auto-update matters only for a `query`-scoped list: with it, every `dcupl.update()` that affects
the list re-runs the seed query, so a new item that matches joins the list and one that stops
matching leaves; without it, the list keeps the keys the query matched at creation (or at the last
`list.update()`). A `listItemKeys` list is the same with or without it: it always holds the current
items for exactly its keys. Unscoped lists are not affected either: every `dcupl.update()` drops
the caches of every affected list, so their queries always run against the current data.

Priority: `dcupl.update({ autoUpdateLists })` > the list's `autoUpdate` >
`performance.autoUpdateLists.enabled` (default `false`). Pass `autoUpdate` through a
`DcuplListOptions` variable, as in the last example above; [Real-time updates](/sdk/advanced/realtime-updates)
walks through the update flow end to end.

## More patterns

A list for a fixed subset takes the `query` or `listItemKeys` option (see [DcuplListOptions](#dcupllistoptions));
for a filter that always follows the current data, call `catalog.query.addCondition()` on an unscoped
list instead ([Catalog API](/sdk/api/catalog#query-operations)). Several filter groups on one list are
covered in [Query logic](/sdk/queries/query-logic). These patterns have their own guides:

- [Master-detail](/sdk/advanced/master-detail): a detail list that follows the selection in a master list
- [Text search](/sdk/queries/query-search): substring and case-insensitive search, escaping user input
- [Faceted search](/sdk/advanced/faceted-search): facets with counts that follow the other filters
- [Pagination & infinite scroll](/sdk/advanced/infinite-scroll): paging with `start` / `count`
- [Real-time updates](/sdk/advanced/realtime-updates): keeping lists in sync with incoming data

## See also

- [Catalog API Reference](/sdk/api/catalog)
- [Dcupl Class API Reference](/sdk/api/dcupl)
- [Data API Reference](/sdk/api/data)
- [Lists](/sdk/core-concepts/lists)
- [Filtering](/sdk/queries/query-filter)
