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)
// Subscribe to all events
const unsubscribe = dcupl.on((message) => {
console.log('Event:', message.type, message);
});
// Later: unsubscribe
unsubscribe();List events
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:
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.
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() to re-render.
// 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.
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:
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.
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.
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() and await
loader.process().
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
showLoadingSpinner();
const unsubscribe = loader.on((progress) => {
updateProgressBar(progress.processed, progress.total);
});
await loader.process();
await dcupl.init();
unsubscribe();
hideLoadingSpinner();Performance monitoring
// 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
// 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:
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
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() instead.
All event types
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 - Main dcupl instance
- Lists API - Working with lists
- Real-time updates - Building reactive UIs