Sitelet https://github.com/sdcorejs/sdcorejs-angular/pull/65
Skip to content

feat(api): API handlers from lazy routes + longest-prefix selection; bump @sdcorejs/utils to 1.2.4 - #65

Merged
sdcorejs merged 3 commits into
mainfrom
chore/utils-1.2.4
Sep 30, 2026
Merged

sdcorejs merged 3 commits into
mainfrom
chore/utils-1.2.4

Conversation

@sdcorejs

@sdcorejs sdcorejs commented Sep 30, 2026 •

Copy link
Copy Markdown
Owner

Summary

1. API handlers from lazy routes (new)

SdHttpInterceptor is registered at the root injector and read SD_API_CONFIG once, at construction. So a library that provides { provide: SD_API_CONFIG, multi: true } in an NgModule loaded with loadChildren lost every handler without any error: no headers, no beforeRemote/afterRemote, no error toast. One Portal hit this with four lazy libraries and worked around it by re-registering every library configuration at root.

  • Libraries do not change. SdApiHandlerRegistry reads the SD_API_CONFIG entries of every injector that the Router creates for a route: Route.providers, or the NgModule of loadChildren.
    • It walks router.config when the registry is created and on every RoutesRecognized. Registration therefore happens in the same navigation, before guards, resolvers and components run.
    • Each injector is read once with { self: true }. Its entries are removed when that injector is destroyed, which includes the router's injector cleanup in Angular 21+.
    • The Router has no public API for route injectors, so Core reads the Router's _injector / _loadedInjector / _loadedRoutes fields. These fields exist in Angular 19–22, and the same spec runs on all four lines. Values of the wrong type are ignored, so a renamed field disables the feature and does not break the app.
    • Limit: a canMatch guard inside the lazy module runs before its scope is registered.
  • SdApiHandlerRegistry (providedIn: 'root') holds runtime registrations. register() returns an unregister function; configurations() is a signal.
  • provideSdApiConfiguration(Class | value): EnvironmentProviders is for values, and for scopes the Router does not create. It registers when the scope's injector is created and unregisters on the scope's DestroyRef. Exported from @sdcorejs/angular/services/api.
  • The interceptor and SdApiService now resolve handlers per request from the static list plus the registry. Existing root SD_API_CONFIG providers keep working unchanged, and SdApiModule still does not call provideHttpClient.
  • Behaviour change (CHANGELOG, with migration note): among matching handlers, the longest matching prefix (origin + path) wins, instead of the first one registered. Ties keep registration order, with root providers first. Host matching itself is unchanged.
  • The token alias and blank-host handling already existed here and were not redone. @sd-angular/core 19.0.41 ships the same API but keeps a raw startsWith match; both CHANGELOGs record this difference.

A handler from a lazy route exists only after its route has been navigated to. A shell that calls a library's host itself before that, for example in a guard on a parent route, must keep a root handler for that host. The docs and the CHANGELOG say so.

2. @sdcorejs/utils 1.2.4

1.2.4 fixes the Observable covariance issue upstream (sdcorejs-utils #14).

Verification

  • npm run check:sync: passes.

  • npm run lint:release on Node 22.22.3: all four lines pass.

  • API specs (services/api/**) for each workspace: 123/123 on v19, v20, v21 and v22.

  • Full ng test --code-coverage for each workspace:

    Workspace Result
    v19 5970/5970, exit 0
    v20 5968/5970: 2 SdImageEditor export/label tests failed under full load
    v21 5970/5970, exit 0
    v22 5967/5970: 3 SdImageEditor export tests failed under full load

    Re-run alone, the SdImageEditor specs pass 114/114 on v20 and on v22. They are the load-sensitive tests already seen flaking before this change, and this PR does not touch them.

  • If the route tracker is disabled, the 5 new lazy-scope tests fail.

🤖 Generated with Claude Code

nghiatt15 and others added 2 commits September 30, 2026 10:53
1.2.4 fixes the root cause of the Observable typing break upstream
(sdcorejs-utils #14). SubscribableLike.subscribe no longer includes null
in its first parameter. The null had blocked TypeScript's contravariant
check against RxJS's single-argument overload, so an Observable of a
narrower type (Observable<string> for string | null | undefined, a
subtype, one union member) was rejected as MaybeAsync<T>. It now fits in
application code too, not only in the Core slots widened in #64.

The explicit MaybeAsync<T> | Observable<T> slots from #64 stay: redundant
with 1.2.4 but harmless, and they keep the 1.1.x contract visible in the
public types. Each lockfile (v19-v22, showcase) changes only the
@sdcorejs/utils entry and keeps its original indentation; v22 stays LF.
CHANGELOG [Unreleased] and sd-query-bar.md are updated.

Verified: check:sync; tsc on v19/v20/v21/v22 and the showcase; v19
library build; v19 suite with coverage 5952/5954. The two failures are
SdImageEditor canvas export specs, which pass alone (114/114) and do not
use @sdcorejs/utils.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and longest-prefix selection

SdHttpInterceptor is registered at the root injector through
HTTP_INTERCEPTORS and resolved inject(SD_API_CONFIG) once, at construction;
SdApiService did the same. A library that provides
{ provide: SD_API_CONFIG, multi: true } in an NgModule loaded with
loadChildren puts it in the lazy route's child injector, which the root
interceptor never sees. Every handler was lost in silence: no headers, no
beforeRemote/afterRemote, no error toast. One Portal hit this with four
lazy libraries and had to re-register every library configuration at root.

- SdApiHandlerRegistry (providedIn root) holds runtime registrations as a
  signal. register() returns an idempotent unregister.
- provideSdApiConfiguration(Class | value): EnvironmentProviders registers
  on scope creation through provideEnvironmentInitializer and unregisters
  through the scope's DestroyRef. A class is provided inside the scope so it
  can inject scoped dependencies. The same declaration works in root
  providers, an eager NgModule, a lazy Route.providers and a loadChildren
  module. It is exported from @sdcorejs/angular/services/api and covered by
  public-api.spec.
- The interceptor and SdApiService resolve per request from the static
  SD_API_CONFIG list plus the registry. Root providers behave as before, and
  SdApiModule still does not call provideHttpClient.
- Behaviour change: among matching handlers, the longest matching host
  prefix (origin + path, via sdMatchesSecureRoute) wins instead of the first
  in registration order; ties keep order, root first. Host matching itself
  is unchanged.

The token alias and blank-host handling (steps D and E) already existed
here. Core Legacy 19.0.41 ships the same registry API and longest-prefix
rule but keeps raw startsWith matching; the CHANGELOG records the
difference.

Specs (api-handler-registry.spec.ts) cover:
- a lazy Route.providers handler after activation;
- a loadChildren NgModule class configuration with a scoped dependency;
- no interception after the scope injector is destroyed;
- root and lazy coexisting with the longest prefix winning, in both orders;
- tie order;
- SdApiService mapResponse from a lazy handler;
- the SD_API_CONFIG / SD_API_CONFIGURATION token alias;
- blank hosts.

Rolled out from v19 to v20/v21/v22 with npm run sync; v22 stays LF.

Verified:
- npm run check:sync: passes.
- npm run lint:release on Node 22.22.3: all four lines pass.
- ng test --code-coverage in v19, v20, v21 and v22: 5964/5964 each,
  exit 0.
- npm run test:scripts: 225/225.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sdcorejs sdcorejs changed the title chore(deps): bump @sdcorejs/utils to 1.2.4 on every line feat(api): API handlers from lazy routes + longest-prefix selection; bump @sdcorejs/utils to 1.2.4 Sep 30, 2026
…brary changes

SdApiHandlerRegistry now reads the SD_API_CONFIG entries of every injector the
Router creates for a route (Route.providers, or the NgModule of loadChildren).
It walks router.config when the registry is created and on each
RoutesRecognized, so registration happens in the same navigation, before
guards, resolvers and components. Each injector is read once, with
{ self: true }, and its entries are removed when the injector is destroyed,
including by the Angular 21+ router injector cleanup.

Libraries keep their plain { provide: SD_API_CONFIG, useClass, multi: true }
provider and do not need to know whether the shell mounts them eagerly or
lazily. provideSdApiConfiguration stays for values and for scopes the Router
does not create. Rolled out to v20, v21 and v22.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sdcorejs
sdcorejs merged commit b5c2e44 into main Sep 30, 2026
10 of 12 checks passed
@sdcorejs
sdcorejs deleted the chore/utils-1.2.4 branch September 30, 2026 10:17
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