Skip to content

ListView observable item updates can crash when replacing large trees from another widget signal #2

Description

@fjdiazt

Project: daz
Feature: scripts-framework

Summary

Morphs Loader exposed a scripts-framework list update hazard: a DzComboBox.textChanged path can cause a large DzListView tree replacement through an observable items$, and DAZ Studio crashes while the list is deleting/rebuilding rows.

This is not a request to remove observable support from list items. Observable<TreeNode[]> is useful and Power Menu uses it successfully. The problem is that the framework currently has no guardrail or explicit mode for large structural list mutations triggered from another widget's active signal chain.

Observed repro

In daz-scripts / Morphs Loader:

  1. Open Morphs Loader with a populated Female manifest.
  2. UI builds a checked tree list with about 56 product roots and 3898 morph leaves.
  3. Change the source dropdown from Genesis 8 & 8.1 Female to Genesis 8 & 8.1 Male.
  4. The Male manifest is missing.
  5. The script tries to replace/clear the product list.
  6. DAZ Studio crashes.

Relevant log tail:

[MorphsLoader][TurboLoader] tree ready products=56 morphs=3898 unknownProducts=25 skippedProducts=0
[MorphsLoader][Dialog] source switch deferred label="Genesis 8 & 8.1 Male"
[MorphsLoader][Source] switch start from="Genesis 8 & 8.1 Female" to="Genesis 8 & 8.1 Male"
[MorphsLoader][TurboLoader] load start path=".../Genesis8_8_1Male.product-manifest.json"
[MorphsLoader][TurboLoader] read failed path=".../Genesis8_8_1Male.product-manifest.json"
[MorphsLoader][Source] switch done to="Genesis 8 & 8.1 Male" manifestLoaded=no
[MorphsLoader][TurboLoader] clear missing-manifest products source="Genesis 8 & 8.1 Male"

Earlier variants crashed when clearing directly inside the combo signal too. Deferring by a short DzTimer was not enough.

Current framework behavior involved

ListViewBuilder.items(model.items$) connects the observable directly to list mutation:

context.items.connect((items) => {
    updateList(items)
})

updateList() does incremental reconciliation against existing DzListViewItems and deletes stale rows. That is fine for many updates, but it is risky for whole-tree replacement/clear after a different widget signal changes state.

Power Menu contrast

Power Menu is a complex stable script using framework list views. Its pattern is safer:

  • list data is prepared before dialog build,
  • UI settings mostly trigger a separate refreshListEvent$,
  • large source-like replacement is not driven directly from a combo's textChanged signal,
  • list refresh is explicit and controlled.

This suggests the framework should keep observable items, but expose/document a safer structural-update mode.

Expected framework guardrail

Need a framework-level design for one or more of these:

  • an opt-in deferred or full-rebuild mode for ListViewBuilder.items(...),
  • a clear distinction between incremental reconcile and full rebuild when the identity universe changes,
  • documentation warning that large tree replacement from another widget signal should use explicit refresh/rebuild, not direct items$ notification,
  • possibly a list mutation queue owned by ListViewBuilder, not by generic Observable.

Avoid changing all Observable dispatch to async/deferred globally. That would be too broad and could break existing scripts.

Acceptance notes

A fix should preserve Power Menu behavior and existing simple observable list usage. The target is a safe framework seam for structural list replacement, not removal of Observable<TreeNode[]> support.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions