Common delete mechanism: route all CF entity deletes through one chokepoint - #5406
Merged
norman-abramovitz merged 10 commits intoJun 2, 2026
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
EntityDeleteControllerchokepoint now wrapswriteWithJob, emits alifecycle event stream (
start → success | failure), and on success:EndpointDataServiceslicestale — descendants (
affectedSlices, containment) ∪ referencingparents/siblings (
referencingSlices, reverse edges, so parent countcolumns and sibling relationship lists refresh),
Why — fixes silently-dropped deletes
The ngrx→signal migration replaced the old schema-complete delete invalidation
with a hand-maintained
cascade-registrythat under-marked: it had noroutesslice at all and never cleaned favorites/recents. Result: after adelete 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
route (routes list + app-attached + app-detail action bar), service
binding (apps config + detach-instance stepper + app-detail action bar).
delete()from eachCnsi*Source; create / update /route-unmap stay.
org/space/app/serviceInstance/serviceBinding.deletecascadekeys.
route.deletestays (fired by route unmap, a relationship op, notan entity delete); create/update + dormant
serviceBroker.*remain.Out of scope (deliberate, follow-up PR): removing the legacy ngrx
RecursiveDeleteEffect/EntityDeleteCompleteActionengine — that needs thelast ngrx-pipeline delete consumer (kube) migrated first. This PR is purely
additive + behavior-preserving + bug-fixing.
Verification
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.