feat(api): API handlers from lazy routes + longest-prefix selection; bump @sdcorejs/utils to 1.2.4 - #65
Merged
Conversation
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>
…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>
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.
Summary
1. API handlers from lazy routes (new)
SdHttpInterceptoris registered at the root injector and readSD_API_CONFIGonce, at construction. So a library that provides{ provide: SD_API_CONFIG, multi: true }in an NgModule loaded withloadChildrenlost every handler without any error: no headers, nobeforeRemote/afterRemote, no error toast. One Portal hit this with four lazy libraries and worked around it by re-registering every library configuration at root.SdApiHandlerRegistryreads theSD_API_CONFIGentries of every injector that the Router creates for a route:Route.providers, or the NgModule ofloadChildren.router.configwhen the registry is created and on everyRoutesRecognized. Registration therefore happens in the same navigation, before guards, resolvers and components run.{ self: true }. Its entries are removed when that injector is destroyed, which includes the router's injector cleanup in Angular 21+._injector/_loadedInjector/_loadedRoutesfields. 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.canMatchguard 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): EnvironmentProvidersis for values, and for scopes the Router does not create. It registers when the scope's injector is created and unregisters on the scope'sDestroyRef. Exported from@sdcorejs/angular/services/api.SdApiServicenow resolve handlers per request from the static list plus the registry. Existing rootSD_API_CONFIGproviders keep working unchanged, andSdApiModulestill does not callprovideHttpClient.@sd-angular/core19.0.41 ships the same API but keeps a rawstartsWithmatch; 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/utils1.2.41.2.4 fixes the Observable covariance issue upstream (sdcorejs-utils #14).
Verification
npm run check:sync: passes.npm run lint:releaseon 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-coveragefor each workspace:SdImageEditorexport/label tests failed under full loadSdImageEditorexport tests failed under full loadRe-run alone, the
SdImageEditorspecs 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