@flyos/design-system 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -48,7 +48,7 @@ import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
48
48
  // tools/publish-library.ps1 at bump time, and asserted by the spec beside this file.
49
49
  // Used only for the diagnostic message; the duplicate-instance detection itself is
50
50
  // version-agnostic, so a stale literal misnames a fork rather than hiding one.
51
- const FLY_DS_VERSION = '1.6.0';
51
+ const FLY_DS_VERSION = '1.7.0';
52
52
  const FLY_DS_REGISTRY_KEY = '__FLY_DS_INSTANCES__';
53
53
  /**
54
54
  * Records this design-system instance on the shared `scope` and returns the
@@ -4768,6 +4768,10 @@ function unloadRemoteStyles(appId) {
4768
4768
  _inFlight.delete(appId);
4769
4769
  }
4770
4770
 
4771
+ /** Accepts a bare class or a `{ default }` module namespace, like Angular's `loadComponent`. */
4772
+ function unwrapLoaded(loaded) {
4773
+ return typeof loaded === 'function' ? loaded : loaded.default;
4774
+ }
4771
4775
  /**
4772
4776
  * Outlet for FlyOS-embedded routing. Renders the component associated with the
4773
4777
  * first matching route in the consumer's `FLY_REMOTE_ROUTES` table, reacting to
@@ -4787,13 +4791,13 @@ function unloadRemoteStyles(appId) {
4787
4791
  * ```
4788
4792
  *
4789
4793
  * Why both? Standalone keeps full Angular routing — query params, guards,
4790
- * resolvers, lazy loading, RouterLink. Embedded gets a stripped-down
4791
- * synchronous outlet that matches against the same route table the remote
4792
- * declared via `FLY_REMOTE_ROUTES`. The same `router.navigate(['/foo', id])`
4793
- * call works in both modes; only the rendering surface differs.
4794
+ * resolvers, RouterLink. Embedded gets a stripped-down outlet that matches
4795
+ * against the same route table the remote declared via `FLY_REMOTE_ROUTES`. The
4796
+ * same `router.navigate(['/foo', id])` call works in both modes; only the
4797
+ * rendering surface differs.
4794
4798
  *
4795
4799
  * Limitations vs. `<router-outlet>`:
4796
- * - Synchronous components only (no `loadComponent` / `loadChildren`).
4800
+ * - No `loadChildren` (route-level code splitting only see `loadComponent`).
4797
4801
  * - No guards / resolvers / data resolution.
4798
4802
  * - No query-param / hash handling — only path segments.
4799
4803
  * - No `RouterLink` directive — use `(click)="router.navigate(...)"`.
@@ -4803,19 +4807,97 @@ function unloadRemoteStyles(appId) {
4803
4807
  * private readonly router = inject(FlyRemoteRouter);
4804
4808
  * readonly id = computed(() => this.router.params()['id'] ?? '');
4805
4809
  * ```
4810
+ *
4811
+ * ## Lazy routes
4812
+ *
4813
+ * A route may supply `loadComponent: () => import('./x').then(m => m.XComponent)`
4814
+ * instead of `component:`. The chunk is fetched on first match and cached for the
4815
+ * lifetime of the outlet, so revisiting a route is synchronous.
4816
+ *
4817
+ * Three behaviours are deliberate, and each exists because the obvious
4818
+ * alternative is worse:
4819
+ *
4820
+ * - **The previous component stays mounted while the next one loads.** Blanking
4821
+ * the outlet first would flash empty on every navigation, and a remote's chunks
4822
+ * are served from its own origin — usually a few milliseconds. This matches
4823
+ * Angular's Router, which does not tear down the current route until the next
4824
+ * one is ready.
4825
+ * - **A stale resolution never wins.** Navigating A → B → C while B is still in
4826
+ * flight must not render B over C. Each resolution carries a sequence token and
4827
+ * is discarded unless it is still the latest.
4828
+ * - **A failed load is reported to `ErrorHandler`, not swallowed.** The
4829
+ * chunk-404-after-redeploy self-heal (`provideFlyChunkReloadRecovery`) lives in
4830
+ * the root `ErrorHandler`, and a rejected promise inside an effect would reach
4831
+ * `unhandledrejection` instead — where that handler never sees it. Routing it
4832
+ * here is what lets a stale embedded remote recover. The failed loader is
4833
+ * evicted so a later attempt genuinely retries rather than reusing the
4834
+ * rejected promise.
4806
4835
  */
4807
4836
  class FlyRemoteRouterOutletComponent {
4808
4837
  router = inject(FlyRemoteRouter);
4838
+ errorHandler = inject(ErrorHandler);
4809
4839
  /**
4810
4840
  * Read directly from FlyRemoteRouter so the outlet re-renders whenever the
4811
4841
  * URL changes (signal-based, OnPush-friendly).
4812
4842
  */
4813
4843
  matched = this.router.matchedRoute;
4844
+ /** Loaders that have resolved — consulted synchronously, so a revisit never flashes. */
4845
+ loaded = new Map();
4846
+ /** Loaders currently in flight, so a param change mid-load doesn't start a second fetch. */
4847
+ inFlight = new Map();
4848
+ /** Bumped per resolution attempt; a late `.then` compares against it and bows out. */
4849
+ seq = 0;
4850
+ component = signal(null, ...(ngDevMode ? [{ debugName: "component" }] : /* istanbul ignore next */ []));
4851
+ /** The component class currently projected into `*ngComponentOutlet`. */
4852
+ rendered = this.component.asReadonly();
4853
+ constructor() {
4854
+ effect(() => {
4855
+ const route = this.matched()?.route ?? null;
4856
+ const token = ++this.seq;
4857
+ if (!route) {
4858
+ this.component.set(null);
4859
+ return;
4860
+ }
4861
+ if (route.component) {
4862
+ this.component.set(route.component);
4863
+ return;
4864
+ }
4865
+ const loader = route.loadComponent;
4866
+ const already = this.loaded.get(loader);
4867
+ if (already) {
4868
+ // Synchronous: no microtask gap, so re-matching the same lazy route on a
4869
+ // param change cannot blank or re-create the component.
4870
+ this.component.set(already);
4871
+ return;
4872
+ }
4873
+ let pending = this.inFlight.get(loader);
4874
+ if (!pending) {
4875
+ pending = Promise.resolve(loader()).then(unwrapLoaded);
4876
+ this.inFlight.set(loader, pending);
4877
+ }
4878
+ pending
4879
+ .then((cmp) => {
4880
+ this.loaded.set(loader, cmp);
4881
+ this.inFlight.delete(loader);
4882
+ if (token === this.seq)
4883
+ this.component.set(cmp);
4884
+ })
4885
+ .catch((err) => {
4886
+ // Evict so a retry re-runs the import instead of re-awaiting a rejection.
4887
+ this.inFlight.delete(loader);
4888
+ // Leave the previous component mounted — a blank outlet tells the user
4889
+ // nothing, and Angular's Router likewise keeps the current route on a
4890
+ // failed lazy load.
4891
+ if (token === this.seq)
4892
+ this.errorHandler.handleError(err);
4893
+ });
4894
+ });
4895
+ }
4814
4896
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyRemoteRouterOutletComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4815
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyRemoteRouterOutletComponent, isStandalone: true, selector: "fly-remote-router-outlet", ngImport: i0, template: `
4816
- @if (matched(); as m) {
4817
- <ng-container *ngComponentOutlet="m.route.component" />
4818
- }
4897
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyRemoteRouterOutletComponent, isStandalone: true, selector: "fly-remote-router-outlet", ngImport: i0, template: `
4898
+ @if (rendered(); as cmp) {
4899
+ <ng-container *ngComponentOutlet="cmp" />
4900
+ }
4819
4901
  `, isInline: true, dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4820
4902
  }
4821
4903
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyRemoteRouterOutletComponent, decorators: [{
@@ -4825,13 +4907,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
4825
4907
  standalone: true,
4826
4908
  imports: [NgComponentOutlet],
4827
4909
  changeDetection: ChangeDetectionStrategy.OnPush,
4828
- template: `
4829
- @if (matched(); as m) {
4830
- <ng-container *ngComponentOutlet="m.route.component" />
4831
- }
4910
+ template: `
4911
+ @if (rendered(); as cmp) {
4912
+ <ng-container *ngComponentOutlet="cmp" />
4913
+ }
4832
4914
  `,
4833
4915
  }]
4834
- }] });
4916
+ }], ctorParameters: () => [] });
4835
4917
 
4836
4918
  /**
4837
4919
  * Shared mock AuthService base class for Business App developers.