@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.
@@ -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
+ };
@@ -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
- var isDev2 = () => {
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 restoreFromCache = (instance, key) => {
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
- try {
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 (!isDev2())
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
- restoreFromCache(instance, fullKey);
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=0D36D7E5DFF7439D64756E2164756E21
14773
+ //# debugId=6CC624544073C0C364756E2164756E21
14767
14774
  //# sourceMappingURL=index.js.map