@absolutejs/absolute 0.19.0-beta.753 → 0.19.0-beta.755

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.
@@ -0,0 +1,223 @@
1
+ /* hmrPreserveCore — shared HMR preservation utilities, with no Angular
2
+ imports. Used by both `preserveAcrossHmr` (the user-facing helper that
3
+ adds OnPush `markForCheck` behavior on top) and the dev-client HMR
4
+ handler (which calls `captureTrackedInstanceStates` before destroying
5
+ the running app and `endHmrReboot` once the new app has stabilized).
6
+
7
+ Splitting these out lets the dev-client HMR handler — which has no
8
+ need for `@angular/core` — call into the same capture/restore logic
9
+ as the user-facing helper without forcing an Angular core import into
10
+ the dev-client bundle. The state itself lives on `globalThis`, so
11
+ both consumers see exactly the same tracker / cache / flag. */
12
+
13
+ export type StateCache = Map<string, Record<string, unknown>>;
14
+ export type InstanceTracker = Set<WeakRef<object>>;
15
+ export type InstanceKeyMap = WeakMap<object, string>;
16
+ export type RebootFlag = { value: boolean };
17
+ export type RebootStats = { captured: number; restoredKeys: Set<string> };
18
+
19
+ type PreserveScope = typeof globalThis & {
20
+ __ABS_HMR_INSTANCE_STATE__?: StateCache;
21
+ __ABS_HMR_TRACKED_INSTANCES__?: InstanceTracker;
22
+ __ABS_HMR_INSTANCE_KEYS__?: InstanceKeyMap;
23
+ __ABS_HMR_REBOOT_IN_PROGRESS__?: RebootFlag;
24
+ __ABS_HMR_REBOOT_STATS__?: RebootStats;
25
+ };
26
+
27
+ export const isHmrPreserveDev = (): boolean => {
28
+ // SSR safety: globalThis on the server is process-wide and shared
29
+ // across requests, so writing to the preservation cache during SSR
30
+ // would leak request state between users. Gate strictly on the
31
+ // presence of a browser `window` *and* a dev signal — neither is
32
+ // true in a production build, so this is a hard no-op there too.
33
+ if (typeof window === 'undefined') return false;
34
+ const scope = globalThis as { __DEV__?: unknown; ngDevMode?: unknown };
35
+
36
+ return Boolean(scope.__DEV__) || Boolean(scope.ngDevMode);
37
+ };
38
+
39
+ export const getCache = (): StateCache => {
40
+ const scope = globalThis as PreserveScope;
41
+
42
+ return (scope.__ABS_HMR_INSTANCE_STATE__ ??= new Map());
43
+ };
44
+
45
+ export const getTracker = (): InstanceTracker => {
46
+ const scope = globalThis as PreserveScope;
47
+
48
+ return (scope.__ABS_HMR_TRACKED_INSTANCES__ ??= new Set());
49
+ };
50
+
51
+ export const getKeyMap = (): InstanceKeyMap => {
52
+ const scope = globalThis as PreserveScope;
53
+
54
+ return (scope.__ABS_HMR_INSTANCE_KEYS__ ??= new WeakMap());
55
+ };
56
+
57
+ export const getRebootFlag = (): RebootFlag => {
58
+ const scope = globalThis as PreserveScope;
59
+
60
+ return (scope.__ABS_HMR_REBOOT_IN_PROGRESS__ ??= { value: false });
61
+ };
62
+
63
+ export const getRebootStats = (): RebootStats => {
64
+ const scope = globalThis as PreserveScope;
65
+
66
+ return (scope.__ABS_HMR_REBOOT_STATS__ ??= {
67
+ captured: 0,
68
+ restoredKeys: new Set()
69
+ });
70
+ };
71
+
72
+ /* Filter for values that are safe to preserve across an HMR full
73
+ re-bootstrap. Snapshots the OLD app's instance state into a cache
74
+ and copies it back onto the NEW instance — but holding references
75
+ to the OLD app's Angular-injected services (HttpClient,
76
+ ApplicationRef, subscriptions tied to the destroyed injector, etc.)
77
+ and restoring them onto the new instance would corrupt the new app:
78
+ the new `HttpClient` from the new injector would be replaced by a
79
+ stale ref pointing at a destroyed graph. So: primitives, plain `{}`
80
+ objects, and arrays of those — covers the common cases (auth tokens,
81
+ cached query results, search queries, pagination state) without
82
+ leaking Angular-injected dependencies, RxJS subjects, DOM nodes, or
83
+ other live references. */
84
+ export const isPreservable = (value: unknown, depth = 0): boolean => {
85
+ if (depth > 8) return false;
86
+ if (value === null || value === undefined) return true;
87
+ const t = typeof value;
88
+ if (t === 'string' || t === 'number' || t === 'boolean' || t === 'bigint')
89
+ return true;
90
+ if (t === 'function' || t === 'symbol') return false;
91
+ if (Array.isArray(value)) {
92
+ return value.every((item) => isPreservable(item, depth + 1));
93
+ }
94
+ if (t === 'object') {
95
+ const proto = Object.getPrototypeOf(value);
96
+ // Only POJOs — class instances (HttpClient, BehaviorSubject, Date,
97
+ // Map, etc.) carry runtime identity that the new instance must
98
+ // get from its own injector / construction.
99
+ if (proto !== Object.prototype && proto !== null) return false;
100
+
101
+ return Object.values(value as object).every((v) =>
102
+ isPreservable(v, depth + 1)
103
+ );
104
+ }
105
+
106
+ return false;
107
+ };
108
+
109
+ export const buildCacheKey = (
110
+ instance: object,
111
+ key?: unknown
112
+ ): string | null => {
113
+ const className = instance.constructor?.name;
114
+ if (!className || className === 'Object') return null;
115
+ const suffix = key === undefined || key === null ? '' : String(key);
116
+
117
+ return `${className}:${suffix}`;
118
+ };
119
+
120
+ /** Copy preservable own properties from the cached snapshot onto the
121
+ * instance. Records the restoration in stats so the end-of-reboot
122
+ * summary can list which classes had state restored. Returns whether
123
+ * anything was actually written. */
124
+ export const restoreFromCacheCore = (
125
+ instance: object,
126
+ key: string
127
+ ): boolean => {
128
+ const cache = getCache();
129
+ const stored = cache.get(key);
130
+ if (!stored) return false;
131
+
132
+ for (const [prop, value] of Object.entries(stored)) {
133
+ try {
134
+ (instance as Record<string, unknown>)[prop] = value;
135
+ } catch {
136
+ /* property is non-writable / has a setter that threw — skip */
137
+ }
138
+ }
139
+
140
+ getRebootStats().restoredKeys.add(key);
141
+
142
+ return true;
143
+ };
144
+
145
+ /** Snapshot every tracked instance's preservable own properties into
146
+ * the shared cache and flip the reboot-in-progress flag on. Called by
147
+ * the dev-client HMR handler right before `destroyAngularApp()`. */
148
+ export const captureTrackedInstanceStates = (): void => {
149
+ if (!isHmrPreserveDev()) return;
150
+
151
+ const cache = getCache();
152
+ const tracker = getTracker();
153
+ const keyMap = getKeyMap();
154
+ const stats = getRebootStats();
155
+ const seen = new Set<string>();
156
+
157
+ cache.clear();
158
+ stats.restoredKeys.clear();
159
+ stats.captured = 0;
160
+
161
+ for (const ref of tracker) {
162
+ const instance = ref.deref();
163
+ // Skip already-GC'd refs. We don't bother bookkeeping a "dead"
164
+ // list because the entire tracker is cleared after this loop.
165
+ if (!instance) continue;
166
+
167
+ const fullKey = keyMap.get(instance) ?? buildCacheKey(instance);
168
+ if (fullKey === null) continue;
169
+
170
+ // Warn when two instances would collide on the same cache slot
171
+ // (same className with no key, or duplicate user-supplied keys).
172
+ // On collision the second instance's state silently overwrites
173
+ // the first — pass an explicit `key` to differentiate.
174
+ if (seen.has(fullKey)) {
175
+ console.warn(
176
+ `[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.`
177
+ );
178
+ }
179
+ seen.add(fullKey);
180
+
181
+ const props: Record<string, unknown> = {};
182
+ for (const prop of Object.keys(instance)) {
183
+ const value = (instance as Record<string, unknown>)[prop];
184
+ if (isPreservable(value)) props[prop] = value;
185
+ }
186
+ cache.set(fullKey, props);
187
+ stats.captured++;
188
+ }
189
+
190
+ // Every instance just captured is about to die: `destroyAngularApp()`
191
+ // runs immediately after this. New instances from the next bootstrap
192
+ // repopulate the tracker via their own `preserveAcrossHmr(this)`
193
+ // calls. Leaving existing WeakRefs in place means the JS engine
194
+ // often won't have GC'd the old objects yet at the next capture —
195
+ // those zombies inflate the captured count and trigger spurious
196
+ // collision warnings against the new generation's instances.
197
+ tracker.clear();
198
+
199
+ getRebootFlag().value = true;
200
+ };
201
+
202
+ /** Clear the active-reboot flag and emit a one-line summary so
203
+ * developers can see at-a-glance which classes had state preserved.
204
+ * Called by the dev-client HMR handler after the new app has reported
205
+ * stable. After this, `preserveAcrossHmr` calls track but don't
206
+ * restore — so navigating to a route after HMR doesn't resurrect
207
+ * stale state from the last reboot. */
208
+ export const endHmrReboot = (): void => {
209
+ if (!isHmrPreserveDev()) return;
210
+ getRebootFlag().value = false;
211
+
212
+ const stats = getRebootStats();
213
+ if (stats.captured > 0) {
214
+ const restored = Array.from(stats.restoredKeys)
215
+ .map((k) => k.replace(/:$/, ''))
216
+ .sort();
217
+ console.info(
218
+ `[HMR] Full re-bootstrap: restored state for ${restored.length}/${stats.captured} tracked instance(s)${
219
+ restored.length > 0 ? ` — ${restored.join(', ')}` : ''
220
+ }. Components without preservation reset to defaults; opt in via \`preserveAcrossHmr(this)\`.`
221
+ );
222
+ }
223
+ };
@@ -14422,8 +14422,11 @@ var preserveAcrossHmr = (instance, key) => {
14422
14422
  const fullKey = buildCacheKey(instance, key);
14423
14423
  if (fullKey === null)
14424
14424
  return;
14425
- getTracker().add(new WeakRef(instance));
14426
- getKeyMap().set(instance, fullKey);
14425
+ const keyMap = getKeyMap();
14426
+ if (!keyMap.has(instance)) {
14427
+ getTracker().add(new WeakRef(instance));
14428
+ }
14429
+ keyMap.set(instance, fullKey);
14427
14430
  if (!getRebootFlag().value)
14428
14431
  return;
14429
14432
  const restored = restoreFromCacheCore(instance, fullKey);
@@ -14770,5 +14773,5 @@ export {
14770
14773
  ABSOLUTE_HTTP_TRANSFER_CACHE_SKIP_HEADER
14771
14774
  };
14772
14775
 
14773
- //# debugId=6CC624544073C0C364756E2164756E21
14776
+ //# debugId=9D9C1F7007C6AC5764756E2164756E21
14774
14777
  //# sourceMappingURL=index.js.map