@absolutejs/absolute 0.19.0-beta.752 → 0.19.0-beta.754
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.
- package/dist/angular/browser.js +20 -13
- package/dist/angular/browser.js.map +5 -4
- package/dist/angular/hmrPreserveCore.ts +223 -0
- package/dist/angular/index.js +20 -13
- package/dist/angular/index.js.map +5 -4
- package/dist/dev/client/handlers/angular.ts +29 -319
- package/dist/src/angular/hmrPreserveCore.d.ts +34 -0
- package/dist/src/angular/preserveAcrossHmr.d.ts +0 -17
- package/package.json +1 -1
|
@@ -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
|
+
};
|
package/dist/angular/index.js
CHANGED
|
@@ -14363,7 +14363,9 @@ var handleAngularPageRequest = async (input) => {
|
|
|
14363
14363
|
var defineAngularPage = (definition) => definition;
|
|
14364
14364
|
// src/angular/preserveAcrossHmr.ts
|
|
14365
14365
|
import { ChangeDetectorRef, inject } from "@angular/core";
|
|
14366
|
-
|
|
14366
|
+
|
|
14367
|
+
// src/angular/hmrPreserveCore.ts
|
|
14368
|
+
var isHmrPreserveDev = () => {
|
|
14367
14369
|
if (typeof window === "undefined")
|
|
14368
14370
|
return false;
|
|
14369
14371
|
const scope = globalThis;
|
|
@@ -14399,34 +14401,39 @@ var buildCacheKey = (instance, key) => {
|
|
|
14399
14401
|
const suffix = key === undefined || key === null ? "" : String(key);
|
|
14400
14402
|
return `${className}:${suffix}`;
|
|
14401
14403
|
};
|
|
14402
|
-
var
|
|
14404
|
+
var restoreFromCacheCore = (instance, key) => {
|
|
14403
14405
|
const cache = getCache();
|
|
14404
14406
|
const stored = cache.get(key);
|
|
14405
14407
|
if (!stored)
|
|
14406
|
-
return;
|
|
14408
|
+
return false;
|
|
14407
14409
|
for (const [prop, value] of Object.entries(stored)) {
|
|
14408
14410
|
try {
|
|
14409
14411
|
instance[prop] = value;
|
|
14410
14412
|
} catch {}
|
|
14411
14413
|
}
|
|
14412
14414
|
getRebootStats().restoredKeys.add(key);
|
|
14413
|
-
|
|
14414
|
-
const cdr = inject(ChangeDetectorRef, { optional: true });
|
|
14415
|
-
if (cdr)
|
|
14416
|
-
queueMicrotask(() => cdr.markForCheck());
|
|
14417
|
-
} catch {}
|
|
14415
|
+
return true;
|
|
14418
14416
|
};
|
|
14417
|
+
|
|
14418
|
+
// src/angular/preserveAcrossHmr.ts
|
|
14419
14419
|
var preserveAcrossHmr = (instance, key) => {
|
|
14420
|
-
if (!
|
|
14420
|
+
if (!isHmrPreserveDev())
|
|
14421
14421
|
return;
|
|
14422
14422
|
const fullKey = buildCacheKey(instance, key);
|
|
14423
14423
|
if (fullKey === null)
|
|
14424
14424
|
return;
|
|
14425
14425
|
getTracker().add(new WeakRef(instance));
|
|
14426
14426
|
getKeyMap().set(instance, fullKey);
|
|
14427
|
-
if (getRebootFlag().value)
|
|
14428
|
-
|
|
14429
|
-
|
|
14427
|
+
if (!getRebootFlag().value)
|
|
14428
|
+
return;
|
|
14429
|
+
const restored = restoreFromCacheCore(instance, fullKey);
|
|
14430
|
+
if (!restored)
|
|
14431
|
+
return;
|
|
14432
|
+
try {
|
|
14433
|
+
const cdr = inject(ChangeDetectorRef, { optional: true });
|
|
14434
|
+
if (cdr)
|
|
14435
|
+
queueMicrotask(() => cdr.markForCheck());
|
|
14436
|
+
} catch {}
|
|
14430
14437
|
};
|
|
14431
14438
|
// src/angular/pendingTask.ts
|
|
14432
14439
|
import { inject as inject2, PendingTasks } from "@angular/core";
|
|
@@ -14763,5 +14770,5 @@ export {
|
|
|
14763
14770
|
ABSOLUTE_HTTP_TRANSFER_CACHE_SKIP_HEADER
|
|
14764
14771
|
};
|
|
14765
14772
|
|
|
14766
|
-
//# debugId=
|
|
14773
|
+
//# debugId=6CC624544073C0C364756E2164756E21
|
|
14767
14774
|
//# sourceMappingURL=index.js.map
|