# Events reference

dcupl uses an event system to notify your application of changes. Subscribe to events using the `on()` method on either the dcupl instance or individual lists.

## Subscribing to events

### Global events (dcupl instance)

```typescript
// Subscribe to all events
const unsubscribe = dcupl.on((message) => {
  console.log('Event:', message.type, message);
});

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

### List events

```typescript
const list = dcupl.lists.create({ modelKey: 'Product' });

// Re-render when the list's result changes
const unsubscribe = list.onResultChange(() => {
  renderProducts(list.catalog.query.items());
});

// Or receive every event of the list, including reads
const off = list.on((message) => {
  console.log('List event:', message.type);
});
```

List events are delivered to `list.on()` only; they do not reach `dcupl.on()`. In the other
direction, every `list.on()` also receives the instance's `dcupl_updated_manually` after each
`dcupl.update()`.

---

## Event message structure

All events share a common message structure:

```typescript
type DcuplUpdateMessage = {
  key: number; // Unique per event (assigned when the call starts, not in delivery order)
  type: DcuplUpdateType; // Event type (see below)
  scope: 'all' | 'global' | 'list' | 'list$'; // emitted events use 'global' or 'list'
  action?: 'update'; // Set on update messages (e.g. result_updated, dcupl_updated_manually)
  origin?: 'user' | 'loader';
  modelKey?: string; // Related model
  listKey?: string; // Related list
  method?: string; // Method that triggered the event
  args?: any[]; // Arguments passed to the method
  result?: any; // Return value of the call: on query_*, fn_* and filter_* events (not query_get, query_has)
  duration?: number; // Execution time in ms, never negative (see below)
  start?: number; // Start timestamp
  end?: number; // End timestamp
};
```

Some events carry their details in `args` instead of `listKey`/`modelKey`: for `list_created`,
`args[0]` holds the create options (`listKey`, `modelKey`); for `data_change`, `args[0]` is the
staged data container (`model`, `type`, `data`).

Every event carries a `duration`: the milliseconds the call or update took, never negative. On
`dcupl_partial_update` it runs from the moment the update starts applying the staged changes.
`key` is assigned when a call starts, so `result_updated`, which is emitted inside the call, is
delivered before the `query_apply` that caused it although it has the higher key.

---

## Event types

### Lifecycle events

| Event                        | Scope  | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| ---------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `dcupl_initialized`          | global | `dcupl.init()` finished, also when it failed and resolved `false`                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `dcupl_destroyed`            | global | dcupl instance was destroyed                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `dcupl_updated_manually`     | global | `dcupl.update()` finished, also when it failed and resolved `false`                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `dcupl_partial_update`       | global | `dcupl.update()` applied staged data changes (partial or full rebuild) or a model definition change (`models.set`/`update`/`remove` after `init()`); not emitted when nothing was staged and no definition changed. Fires before `dcupl_updated_manually`                                                                                                                                                                                                                                                                              |
| `dcupl_handle_datacontainer` | global | Data containers processed during `init()` or a full rebuild in `update()` (`args[0]` is the array of containers). A partial update emits it, with just the `$DcuplErrorTrackingErrors` and `$DcuplErrorTrackingSummary` containers, whenever the model has recorded data quality errors, whether or not they changed. While no list over `$DcuplErrorTrackingErrors`/`Summary` exists, their containers are processed at the first read of those models instead of during `init()`/`update()`, and the event for that build fires then |

`dcupl_partial_update` carries the change in `args[0]`:
`{ isPartialUpdate: boolean, changedModels: string[], changedAttributes: Map<string, Set<string>>, affectedModels: string[] }`.
`isPartialUpdate` is `true` for a partial update and `false` when the update fell back to a full
rebuild (for example after `data.set()`). `affectedModels` adds the models that
reference a changed model.

`changedModels` also names a model whose definition changed since the last `init()` or full
`update()`: after `models.set()` or `models.update()` every property and reference of the new
definition is in `changedAttributes`, after `models.remove()` the entry is an empty set. A
definition change always rebuilds everything, so `isPartialUpdate` is `false`.

```typescript
dcupl.on((msg) => {
  if (msg.type === 'dcupl_initialized') {
    // Fires even when init() failed: check the value init() resolved with
    console.log('dcupl.init() finished');
  }
  if (msg.type === 'dcupl_partial_update') {
    console.log('Changed models:', msg.args[0].changedModels);
  }
});
```

### List events

| Event            | Scope  | Description                                                                                           |
| ---------------- | ------ | ----------------------------------------------------------------------------------------------------- |
| `list_created`   | global | New list was created (`args[0]` holds `listKey` and `modelKey`)                                       |
| `list_updated`   | list   | `list.update()` re-derived the list — called by you, or by `dcupl.update()` for an auto-updating list |
| `list_destroyed` | list   | List was destroyed                                                                                    |

`list_updated` is not the "result changed" signal: use `result_updated` /
[`list.onResultChange()`](/sdk/api/lists#onresultchange) to re-render.

```typescript
// Track all list creations
dcupl.on((msg) => {
  if (msg.type === 'list_created') {
    const { listKey, modelKey } = msg.args[0];
    console.log('New list:', listKey, 'for model:', modelKey);
  }
});

// Re-render a list when its result changes
const list = dcupl.lists.create({ modelKey: 'Product' });
list.onResultChange(() => {
  updateProductDisplay();
});
```

### Query events

| Event                 | Scope | Description                                                                                      |
| --------------------- | ----- | ------------------------------------------------------------------------------------------------ |
| `query_apply`         | list  | Query condition added/modified (`apply`, `addCondition`, `setCondition`, `addGroup`, `setGroup`) |
| `query_apply_options` | list  | Query options changed (`applyOptions`)                                                           |
| `query_remove`        | list  | Query condition removed (`remove`, `removeCondition`, `removeGroup`)                             |
| `query_reset`         | list  | Query cleared (`reset`, `clear`)                                                                 |
| `query_execute`       | list  | Query executed (`execute`, `items`, `first`, `any`, `count`)                                     |
| `query_get`           | list  | Query state retrieved (`get`, `current`, `isEmpty`)                                              |
| `query_has`           | list  | Query existence checked (`has`)                                                                  |
| `query_one`           | list  | Single item retrieved (`one`)                                                                    |
| `query_many`          | list  | Multiple items retrieved (`many`)                                                                |
| `result_updated`      | list  | Query results changed                                                                            |

`dcupl.query.execute()`, `dcupl.query.one()` and `dcupl.query.many()` emit `query_execute`,
`query_one` and `query_many` with scope `global` on `dcupl.on()`, with the return value in `result`.

```typescript
list.on((msg) => {
  switch (msg.type) {
    case 'query_apply':
      console.log('Filter applied:', msg.args);
      break;
    case 'result_updated':
      console.log('Results updated, refreshing...');
      refreshUI();
      break;
  }
});
```

`result_updated` is the list's "result changed" signal:

| Trigger                                                                                 | `result_updated` on a list                                                                                                                                            |
| --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Query change (`addCondition`, `apply`, `remove`, `reset`, …)                            | once (never with `{ skipProcessing: true }`)                                                                                                                          |
| `dcupl.update()` with pending changes (`data.update` / `data.set` / remove)             | once per affected list: lists of changed models, of models referencing them, and lists whose query, facets or aggregations read a dotted path through a changed model |
| `dcupl.update()` with nothing pending, and re-`init()`                                  | once on every list                                                                                                                                                    |
| Read-only calls (`execute`, `fn.facets`, `fn.aggregate`, `fn.groupBy`, `fn.suggest`, …) | never                                                                                                                                                                 |
| Manual `list.update()`                                                                  | never (emits `list_updated`)                                                                                                                                          |

During `dcupl.update()`, every affected list is refreshed before `result_updated` is emitted, so a handler that re-queries sees the updated results.

`list.onResultChange(cb)` is a shortcut for `list.on` filtered to `result_updated`. It returns an unsubscribe function:

```typescript
const unsubscribe = list.onResultChange(() => refreshUI());
// later
unsubscribe();
```

### Data events

| Event         | Scope  | Description                                                                                                                                               |
| ------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `data_change` | global | Data was staged (`data.set`/`update`/`upsert`/`remove`/`reset`/`apply`, loader data, internal `$Dcupl…` models)                                           |
| `model_add`   | global | A model definition was registered or changed (`models.set`/`update`, also for the loader's model files and the internal `$Dcupl…` models during `init()`) |
| `view_add`    | global | A view was registered or replaced (`views.set`)                                                                                                           |

`data_change` fires when the data is staged, before `dcupl.update()` or `dcupl.init()` applies it.
To react to applied data, use `dcupl_updated_manually` or `list.onResultChange()`.

`models.remove()` and `views.remove()` emit no global event (`models.remove()` destroys the model's
lists, which emit `list_destroyed` to `list.on()`). After `models.remove()`, the next
`dcupl.update()` names the removed model in [`dcupl_partial_update`](/sdk/api/events#lifecycle-events).

```typescript
dcupl.on((msg) => {
  if (msg.type === 'data_change') {
    const { model, type } = msg.args[0]; // the staged data container
    console.log(`Staged ${type} for model:`, model);
  }
});
```

### Analytics function events

| Event          | Scope | Description                |
| -------------- | ----- | -------------------------- |
| `fn_facets`    | list  | Facets calculated          |
| `fn_aggregate` | list  | Aggregation computed       |
| `fn_suggest`   | list  | Suggestions generated      |
| `fn_groupBy`   | list  | GroupBy executed           |
| `fn_pivot`     | list  | Pivot table computed       |
| `fn_metadata`  | list  | Metadata retrieved         |
| `filter_one`   | list  | `catalog.filters.get()`    |
| `filter_many`  | list  | `catalog.filters.getAll()` |

The global `dcupl.fn.*` functions emit the same `fn_*` types with scope `global` on `dcupl.on()`,
with the return value in `result`.

```typescript
list.on((msg) => {
  if (msg.type === 'fn_facets') {
    console.log(`Facets computed in ${msg.duration}ms`);
  }
});
```

### Loader events

| Event                    | Reaches `dcupl.on()` | Description                                                         |
| ------------------------ | -------------------- | ------------------------------------------------------------------- |
| `loader_added`           | yes                  | Loader registered with `dcupl.loaders.add()` (`args[0]` is its key) |
| `loader_removed`         | yes                  | Loader removed                                                      |
| `loader_config_fetched`  | no                   | Emitted on the loader itself when its config was fetched            |
| `loader_processed`       | no                   | Emitted on the loader itself when `loader.process()` finished       |
| `loader_models_fetched`  | no                   | Part of the type; not emitted                                       |
| `loader_data_fetched`    | no                   | Part of the type; not emitted                                       |
| `loader_scripts_fetched` | no                   | Part of the type; not emitted                                       |

To track loading, use the loader's progress callback [`loader.on()`](/sdk/api/loader#on) and await
`loader.process()`.

```typescript
dcupl.on((msg) => {
  if (msg.type === 'loader_added') {
    console.log('Loader registered:', msg.args[0]);
  }
});

// Loading progress comes from the loader itself
loader.on((progress) => {
  console.log(`${progress.processed}/${progress.total} resources processed`);
});
```

---

## Common patterns

### Loading indicator

```typescript
showLoadingSpinner();

const unsubscribe = loader.on((progress) => {
  updateProgressBar(progress.processed, progress.total);
});

await loader.process();
await dcupl.init();

unsubscribe();
hideLoadingSpinner();
```

### Performance monitoring

```typescript
// duration: milliseconds the call took (see Event Message Structure)
dcupl.on((msg) => {
  if (msg.duration && msg.duration > 100) {
    console.warn(`Slow operation: ${msg.type} took ${msg.duration}ms`, {
      model: msg.modelKey,
      list: msg.listKey,
      args: msg.args,
    });
  }
});
```

### Debugging

```typescript
// Log all events in development
if (process.env.NODE_ENV === 'development') {
  dcupl.on((msg) => {
    console.log(`[dcupl] ${msg.type}`, {
      scope: msg.scope,
      model: msg.modelKey,
      list: msg.listKey,
      duration: msg.duration ? `${msg.duration}ms` : undefined,
    });
  });
}
```

### Sync with external systems

`data_change` has no `origin` for data calls: it fires for your own `data.*` calls, for data the
loader adds, and for dcupl's internal `$Dcupl…` models. Filter on the staged container's model:

```typescript
dcupl.on(async (msg) => {
  if (msg.type !== 'data_change') return;

  const { model, type, data } = msg.args[0];
  if (model === 'Order') {
    // A change to Order was staged, sync it to the server
    await syncToServer(model, type, data);
  }
});
```

### React integration

```typescript
function useDcuplEvent(dcupl: Dcupl | null, eventType: DcuplUpdateType) {
  const [lastEvent, setLastEvent] = useState<DcuplUpdateMessage | null>(null);

  useEffect(() => {
    if (!dcupl) return;

    const unsubscribe = dcupl.on((msg) => {
      if (msg.type === eventType) {
        setLastEvent(msg);
      }
    });

    return unsubscribe;
  }, [dcupl, eventType]);

  return lastEvent;
}

// Usage: dcupl_updated_manually fires after each dcupl.update() has been applied
function ProductList() {
  const event = useDcuplEvent(dcupl, 'dcupl_updated_manually');

  useEffect(() => {
    if (event) {
      refetchProducts();
    }
  }, [event]);
}
```

For a component bound to one list, subscribe with
[`list.onResultChange()`](/sdk/api/lists#onresultchange) instead.

---

## All event types

```typescript
type DcuplUpdateType =
  // Lifecycle
  | 'dcupl_initialized'
  | 'dcupl_destroyed'
  | 'dcupl_updated_manually'
  | 'dcupl_partial_update'
  | 'dcupl_handle_datacontainer'
  // Data
  | 'data_change'
  | 'model_add'
  | 'view_add'
  // Lists
  | 'list_created'
  | 'list_updated'
  | 'list_destroyed'
  // Queries
  | 'query_apply'
  | 'query_apply_options'
  | 'query_remove'
  | 'query_reset'
  | 'query_execute'
  | 'query_get'
  | 'query_has'
  | 'query_one'
  | 'query_many'
  | 'result_updated'
  // Analytics
  | 'fn_facets'
  | 'fn_aggregate'
  | 'fn_suggest'
  | 'fn_groupBy'
  | 'fn_pivot'
  | 'fn_metadata'
  | 'filter_one'
  | 'filter_many'
  // Loaders
  | 'loader_added'
  | 'loader_removed'
  | 'loader_config_fetched'
  | 'loader_data_fetched'
  | 'loader_models_fetched'
  | 'loader_scripts_fetched'
  | 'loader_processed';
```

---

## See also

- [Dcupl API](/sdk/api/dcupl) - Main dcupl instance
- [Lists API](/sdk/api/lists) - Working with lists
- [Real-time updates](/sdk/advanced/realtime-updates) - Building reactive UIs
