@absolutejs/absolute 0.19.0-beta.749 → 0.19.0-beta.750

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.
@@ -694,18 +694,33 @@ const handleFullUpdate = async (message: HMRMessage) => {
694
694
 
695
695
  const doUpdate = async () => {
696
696
  // Snapshot every instance that opted into `preserveAcrossHmr(this)`
697
- // before destroying the app. The new instances created during
698
- // bootstrap will read these values back via the same helper, so
699
- // service state (auth tokens, cached data, subscriptions held by
700
- // reference) survives a full re-bootstrap. Generic by class name
701
- // — no per-app configuration here.
697
+ // before destroying the app, and flip the reboot-in-progress flag
698
+ // on. The new instances created during bootstrap will read cached
699
+ // state back via the same helper while the flag is on.
702
700
  captureHmrPreservedInstanceStates();
703
- destroyAngularApp();
704
- await bootstrapAngularModule(indexPath, rootSelector, rootContainer);
705
- restoreComponentState(componentState);
706
- tickAngularApp();
707
- restoreFormState(formState);
708
- restoreScrollState(scrollState);
701
+ try {
702
+ destroyAngularApp();
703
+ await bootstrapAngularModule(
704
+ indexPath,
705
+ rootSelector,
706
+ rootContainer
707
+ );
708
+ restoreComponentState(componentState);
709
+ tickAngularApp();
710
+ restoreFormState(formState);
711
+ restoreScrollState(scrollState);
712
+ } finally {
713
+ // Lazy-loaded child route components construct AFTER
714
+ // `bootstrapAngularModule` returns — the route activation
715
+ // chain (loadComponent → dynamic import → instantiate) runs
716
+ // asynchronously after the root app reports bootstrapped.
717
+ // Keep the flag on for a brief grace window so those lazy
718
+ // components also restore from cache. 750ms is generous
719
+ // enough for typical chunk loads (~50-200ms cold) and short
720
+ // enough that a user can't navigate within the window —
721
+ // preventing stale-state on subsequent navigations.
722
+ setTimeout(endHmrPreservedReboot, 750);
723
+ }
709
724
  };
710
725
 
711
726
  await runWithViewTransition(doUpdate);
@@ -727,6 +742,8 @@ const handleFullUpdate = async (message: HMRMessage) => {
727
742
  type HmrPreserveScope = typeof globalThis & {
728
743
  __ABS_HMR_INSTANCE_STATE__?: Map<string, Record<string, unknown>>;
729
744
  __ABS_HMR_TRACKED_INSTANCES__?: Set<WeakRef<object>>;
745
+ __ABS_HMR_INSTANCE_KEYS__?: WeakMap<object, string>;
746
+ __ABS_HMR_REBOOT_IN_PROGRESS__?: { value: boolean };
730
747
  };
731
748
 
732
749
  const isPreservableValue = (value: unknown, depth = 0): boolean => {
@@ -754,10 +771,25 @@ const isPreservableValue = (value: unknown, depth = 0): boolean => {
754
771
  const captureHmrPreservedInstanceStates = () => {
755
772
  const scope = globalThis as HmrPreserveScope;
756
773
  const tracker = scope.__ABS_HMR_TRACKED_INSTANCES__;
757
- if (!tracker || tracker.size === 0) return;
774
+
775
+ const flag = (scope.__ABS_HMR_REBOOT_IN_PROGRESS__ ??= { value: false });
776
+
777
+ if (!tracker || tracker.size === 0) {
778
+ // Nothing tracked, but still flip the flag so any service/component
779
+ // that registers DURING the reboot (`preserveAcrossHmr` in
780
+ // constructor) knows it's in a reboot context — even though there's
781
+ // nothing to restore from.
782
+ flag.value = true;
783
+
784
+ return;
785
+ }
758
786
 
759
787
  const cache = (scope.__ABS_HMR_INSTANCE_STATE__ ??= new Map());
788
+ const keyMap = scope.__ABS_HMR_INSTANCE_KEYS__;
760
789
  const dead: WeakRef<object>[] = [];
790
+ const seen = new Set<string>();
791
+
792
+ cache.clear();
761
793
 
762
794
  for (const ref of tracker) {
763
795
  const instance = ref.deref();
@@ -766,16 +798,32 @@ const captureHmrPreservedInstanceStates = () => {
766
798
  continue;
767
799
  }
768
800
 
769
- const key = instance.constructor?.name;
770
- if (!key || key === 'Object') continue;
801
+ const className = instance.constructor?.name;
802
+ if (!className || className === 'Object') continue;
803
+ const fullKey = keyMap?.get(instance) ?? `${className}:`;
804
+
805
+ if (seen.has(fullKey)) {
806
+ console.warn(
807
+ `[HMR] preserveAcrossHmr collision on "${fullKey}". Two instances would use the same cache slot — the later one will overwrite the earlier one's state on full re-bootstrap. Pass a unique \`key\` argument (e.g. an @Input id) to differentiate.`
808
+ );
809
+ }
810
+ seen.add(fullKey);
771
811
 
772
812
  const props: Record<string, unknown> = {};
773
813
  for (const prop of Object.keys(instance)) {
774
814
  const value = (instance as Record<string, unknown>)[prop];
775
815
  if (isPreservableValue(value)) props[prop] = value;
776
816
  }
777
- cache.set(key, props);
817
+ cache.set(fullKey, props);
778
818
  }
779
819
 
780
820
  dead.forEach((ref) => tracker.delete(ref));
821
+
822
+ flag.value = true;
823
+ };
824
+
825
+ const endHmrPreservedReboot = () => {
826
+ const scope = globalThis as HmrPreserveScope;
827
+ const flag = scope.__ABS_HMR_REBOOT_IN_PROGRESS__;
828
+ if (flag) flag.value = false;
781
829
  };
@@ -1,10 +1,25 @@
1
- /** Mark a service instance for state preservation across full Angular HMR
2
- * re-bootstraps. Call once from the constructor. Safe in production
3
- * (no-op outside dev mode). */
4
- export declare const preserveAcrossHmr: (instance: object) => void;
1
+ /** Mark a service or component instance for state preservation across
2
+ * full Angular HMR re-bootstraps. Call once from the constructor or
3
+ * `ngOnInit`. Safe in production (no-op outside dev mode).
4
+ *
5
+ * @param instance Usually `this`. The class name is used as part of
6
+ * the cache key.
7
+ * @param key Optional discriminator when multiple instances of the
8
+ * same class can be alive at once (rows, tabs, etc). Coerced
9
+ * to string. Use `ngOnInit` to call this when the key depends
10
+ * on `@Input` values, since Angular sets inputs between
11
+ * constructor and ngOnInit. */
12
+ export declare const preserveAcrossHmr: (instance: object, key?: string | number) => void;
5
13
  /** Snapshot every tracked instance's preservable own properties into
6
- * the cache. Called by the HMR client right before `destroyAngularApp()`.
7
- * Properties that aren't preservable (Angular-injected services,
8
- * Subjects, class instances) are skipped — the new instance gets fresh
9
- * ones from its new injector, just like at first bootstrap. */
14
+ * the cache and flip the reboot flag on. Called by the HMR client
15
+ * right before `destroyAngularApp()`. Properties that aren't
16
+ * preservable (Angular-injected services, Subjects, class instances)
17
+ * are skipped the new instance gets fresh ones from its new
18
+ * injector, just like at first bootstrap. */
10
19
  export declare const captureTrackedInstanceStates: () => void;
20
+ /** Clear the active-reboot flag. Called by the HMR client after the
21
+ * new app has finished bootstrapping (success or failure — wrap in
22
+ * `try/finally`). After this, `preserveAcrossHmr` calls will track
23
+ * but won't restore — so navigating to a route after HMR doesn't
24
+ * resurrect stale state from the last reboot. */
25
+ export declare const endHmrReboot: () => void;
package/package.json CHANGED
@@ -349,5 +349,5 @@
349
349
  ]
350
350
  }
351
351
  },
352
- "version": "0.19.0-beta.749"
352
+ "version": "0.19.0-beta.750"
353
353
  }