@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
|
|
698
|
-
//
|
|
699
|
-
//
|
|
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
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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
|
-
|
|
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
|
|
770
|
-
if (!
|
|
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(
|
|
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
|
|
2
|
-
* re-bootstraps. Call once from the constructor
|
|
3
|
-
* (no-op outside dev mode).
|
|
4
|
-
|
|
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
|
|
7
|
-
* Properties that aren't
|
|
8
|
-
* Subjects, class instances)
|
|
9
|
-
*
|
|
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