Skip to content

Common delete mechanism: route all CF entity deletes through one chokepoint - #5406

Merged
norman-abramovitz merged 10 commits into
cloudfoundry:developfrom
nabramovitz:refactor/entity-delete-mechanism
Jun 2, 2026
Merged

Common delete mechanism: route all CF entity deletes through one chokepoint#5406
norman-abramovitz merged 10 commits into
cloudfoundry:developfrom
nabramovitz:refactor/entity-delete-mechanism

Conversation

@nabramovitz

Copy link
Copy Markdown
Contributor

What

Introduces a common entity-delete mechanism for Cloud Foundry and routes
every CF entity delete through it, replacing the hand-curated per-mutation
cascade table with invalidation derived from the relation graph.

A single EntityDeleteController chokepoint now wraps writeWithJob, emits a
lifecycle event stream (start → success | failure), and on success:

  • walks the relation graph to mark every affected EndpointDataService slice
    stale — descendants (affectedSlices, containment) ∪ referencing
    parents/siblings
    (referencingSlices, reverse edges, so parent count
    columns and sibling relationship lists refresh),
  • removes the deleted row from the local cache,
  • fires cleanup hooks that drop the entity's favorite + recents entries.

Why — fixes silently-dropped deletes

The ngrx→signal migration replaced the old schema-complete delete invalidation
with a hand-maintained cascade-registry that under-marked: it had no
routes slice at all
and never cleaned favorites/recents. Result: after a
delete the server-side entity was gone but the UI kept stale lists and a
stranded favorite on Home. This reproduced live (create+favorite an org →
delete → org 404 server-side but still in the cache, favorite stranded).

The relation-graph derivation closes that gap (routes now covered) and the
cleanup hooks restore the favorite/recents removal the migration dropped.

Scope

  • Routed through the controller: org, space, application, service-instance,
    route
    (routes list + app-attached + app-detail action bar), service
    binding
    (apps config + detach-instance stepper + app-detail action bar).
  • Removed the now-orphaned delete() from each Cnsi*Source; create / update /
    route-unmap stay.
  • Pruned the dead org/space/app/serviceInstance/serviceBinding.delete cascade
    keys. route.delete stays (fired by route unmap, a relationship op, not
    an entity delete); create/update + dormant serviceBroker.* remain.

Out of scope (deliberate, follow-up PR): removing the legacy ngrx
RecursiveDeleteEffect / EntityDeleteCompleteAction engine — that needs the
last ngrx-pipeline delete consumer (kube) migrated first. This PR is purely
additive + behavior-preserving + bug-fixing.

Verification

  • Full gate green: 2367 unit tests (3 skipped), lint, production build.
  • Live-verified end-to-end on a lab CF for both backing patterns: an
    EDS-backed delete (org) and an orchestrator-backed delete (app) — delete-event
    success fired, entity left the cache + list, stranded favorite removed,
    server-side absent.

Note

A separate pre-existing gap surfaced during testing — the per-CF "cf app wall"
is missing Org/Space columns (column-config, not data). Recorded in #5400; not
addressed here.

Introduces DeleteEvent types and EntityDeleteController.delete() which
emits start→success or start→failure via a ReplaySubject, resolving
done with the terminal event. writeWithJob is injected via
WRITE_WITH_JOB token so specs supply a fake without real HTTP.
…helper

Refactor EntityDeleteController.delete() to a bare async IIFE (drop the
new Promise wrapper) with safeNext() wrapping subject.next() calls so a
synchronous observer throw cannot prevent done from resolving. Add the
subscriber-throw regression test (RxJS onUnhandledError guard + macrotask
drain). Explicit ReplaySubject buffer size of 3. JSDoc on reason? field.
Removes the unused tick helper from the spec.
Pure affectedSlices(rootEntityType, registry) walks the
RelationDescriptorRegistry in a single DFS, collecting every
transitively-reachable child entity type and mapping it to its
EDS slice name via an explicit entityType→slice table.

14 vitest specs green; no Angular, no HTTP, no hardcoded relation
table — correct-by-construction from the registered graph.
Register the real org/space/app/route/serviceInstance relation graph into
SignalRelationFetcherService at CF package bootstrap so the entity-delete
chokepoint can derive a complete invalidation closure via affectedSlices().

Closes the gap the legacy hand-curated cascade-registry had: org/space
deletes now invalidate the routes slice (affectedSlices('organization')
covers spaces/apps/routes/serviceInstances/serviceCredentialBindings).

fetchChildren impls are thin (delete invalidation needs only the parent->
child edges); live fetch wiring is wave B.
On a successful delete the EntityDeleteController now walks the relation
graph (affectedSlices) for the deleted entity, marks every affected
EndpointDataService slice stale, removes the deleted row, and fires
registered cleanup hooks (favorites/recents, wired in a later task).
Emits a 'delete-event' diagnostics counter per lifecycle transition.

Supporting changes:
- EndpointDataRegistry.peek(): side-effect-free instance lookup.
- SignalRelationFetcherService.snapshotRegistry() made public.
- EndpointDataService: add routes stale flag + markStale('routes') case,
  wired into load()'s warm-cache guard so a route-cascade refetches the
  count. EntityKind gains 'routes' — the slice the old cascade-registry
  never marked.
- diagnostics: add 'delete-event' code family.
deleteOrg now builds a DeleteRequest and goes through the root-singleton
EntityDeleteController instead of CnsiOrgsSource. The controller derives
the complete invalidation set from the relation graph (now including
routes) and fires cleanup hooks.

Fixes the cold/fallback path: delete no longer depends on initialize()
having run, so deep-linking the org summary page (without the org-list
tab ever mounting) still cleans up caches — the reproduced bug.

- Remove the now-orphaned CnsiOrgsSource.delete (+ its two spec cases);
  create/update stay until Increment 2 fans the rest of the entities.
- Thread the org name through both delete call sites for richer events.
Register a delete cleanup hook that removes the stranded favorite and
recents entry for a deleted entity — the cleanup the ngrx->signal delete
migration silently dropped (favorites lingered on Home; the reproduced
bug). The hook rebuilds the UserFavorite identity and dispatches the
existing RemoveUserFavoriteAction plus a new focused RemoveRecentEntityAction.

- store: add RemoveRecentEntityAction + reducer case (per-entity recents
  removal; replaces the recents side-effect of the retiring
  EntityDeleteCompleteAction without the RecursiveDelete coupling).
- export RemoveUserFavoriteAction + RemoveRecentEntityAction from public-api.
- wire the hook into the controller at CF package bootstrap.
Increment 2 prep. The controller now unions affectedSlices (descendants,
containment) with referencingSlices (reverse edges, one hop) so a delete
also refreshes parent count columns and sibling relationship lists — e.g.
deleting a space refreshes the org's space-count; deleting a binding
refreshes bound apps + instances. This preserves and corrects the
non-containment cascades the legacy cascade-registry hand-curated
(route->apps, serviceBinding->apps+serviceInstances), still derived from
one graph. Count-only slices (routes) with no row remover mark their own
slice stale instead.
Increment 2 fan-out. Every primary entity-delete entry point now goes
through the controller chokepoint via a shared runCfDelete helper, so
each delete gets graph-derived invalidation (descendants + referencing
parents) plus favorites/recents cleanup — one implementation, no
hand-curated per-entity cascade.

Routed: org (refactored to helper), space, app, serviceInstance, route
(routes list + app-attached), serviceBinding (apps config + the
detach-service-instance stepper). Orchestrator-backed entities (app/SI)
also call orchestrator.removeRow to drop the list row immediately.

Removed the now-orphaned delete() from each Cnsi*Source (+ spec cleanup);
create/update/unmap/action stay. cascade-registry is retained for
create/update + route unmap, which still use applyCascade.

Deferred: app-detail unmap/unbind action services and the cascade-registry
delete-key teardown.

Gate: 2369 unit tests, lint, prod build all green.
…ade keys

Completes the Increment-2 fan-out. The two remaining entity-delete entry
points on the app-detail action bar now route through EntityDeleteController:
- AppRouteActionsService.deleteRoute (route entity)
- AppServiceBindingActionsService.unbindService (binding entity)
Both previously issued a bare writeWithJob with NO invalidation — the same
silently-stale bug, now fixed via the chokepoint. Route unmap/attach stay as
relationship ops (not entity deletes).

Prune cascade-registry: with all entity deletes now graph-derived, the
org/space/app/serviceInstance/serviceBinding '.delete' keys are dead and
removed. route.delete stays (fired by route unmap); create/update stay;
serviceBroker.* left as dormant parked-UI scaffolding. EDS spec's two
obsolete delete-cascade tests replaced with surviving-cascade coverage.

Gate: 2367 unit tests, lint, prod build all green.

@norman-abramovitz norman-abramovitz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@norman-abramovitz
norman-abramovitz merged commit 9d83602 into cloudfoundry:develop Jun 2, 2026
12 checks passed
@nabramovitz
nabramovitz deleted the refactor/entity-delete-mechanism branch June 17, 2026 08:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants