@braidlabs/angular-router 0.3.0 → 0.5.0
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.
|
@@ -1,490 +1,301 @@
|
|
|
1
|
-
import * as i0 from
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { isPlatformBrowser } from
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
const SKEW_RECOVERY_OPTIONS = new InjectionToken('SKEW_RECOVERY_OPTIONS');
|
|
1
|
+
import * as i0 from "@angular/core";
|
|
2
|
+
import { DOCUMENT, DestroyRef, Injectable, InjectionToken, PLATFORM_ID, computed, inject, makeEnvironmentProviders, provideEnvironmentInitializer, signal } from "@angular/core";
|
|
3
|
+
import { createVersionProbe, isSkewDisabled, moduleWasRemoved, setSkewDisabled } from "@braidlabs/skew";
|
|
4
|
+
import { isPlatformBrowser } from "@angular/common";
|
|
5
|
+
import { NavigationError, Router } from "@angular/router";
|
|
6
|
+
const SKEW_RECOVERY_OPTIONS = new InjectionToken("SKEW_RECOVERY_OPTIONS");
|
|
8
7
|
function resolveOptions(options) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
8
|
+
return {
|
|
9
|
+
...options,
|
|
10
|
+
onStaleChunk: options.onStaleChunk ?? "reload-at-target",
|
|
11
|
+
retryAttempts: options.retryAttempts ?? 1,
|
|
12
|
+
retryDelayMs: options.retryDelayMs ?? 250,
|
|
13
|
+
maxRecoveries: options.maxRecoveries ?? 1,
|
|
14
|
+
fallbackRoute: options.fallbackRoute ?? "/",
|
|
15
|
+
respectUnsavedWork: options.respectUnsavedWork ?? true
|
|
16
|
+
};
|
|
18
17
|
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Wrapping the dynamic import.
|
|
22
|
-
*
|
|
23
|
-
* Two problems are solved here, both before any Angular machinery is involved:
|
|
24
|
-
*
|
|
25
|
-
* 1. **Attribution.** By the time an import rejects, the runtime holds
|
|
26
|
-
* `chunk-ABC123.js` — not `./admin/routes`. Without a logical id there is no
|
|
27
|
-
* way to ask the new build's manifest whether that route still exists.
|
|
28
|
-
* 2. **Transience.** A flaky network, a CDN edge miss, and a genuinely purged
|
|
29
|
-
* asset are indistinguishable from the error alone. A bounded retry resolves
|
|
30
|
-
* the first two without inflicting a page reload on anyone.
|
|
31
|
-
*
|
|
32
|
-
* Retry is therefore a *precondition* to recovery, not one of the strategies.
|
|
33
|
-
*/
|
|
34
|
-
/** Defaults for `lazy()`, overridable globally by `provideSkewRecovery()`. */
|
|
35
18
|
const lazyDefaults = {
|
|
36
|
-
|
|
37
|
-
|
|
19
|
+
retryAttempts: 1,
|
|
20
|
+
retryDelayMs: 250
|
|
38
21
|
};
|
|
39
|
-
/** Restores defaults. Exposed for tests, which must not leak configuration. */
|
|
40
22
|
function resetLazyDefaults() {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Thrown when a lazy load fails after its retries are exhausted.
|
|
46
|
-
*
|
|
47
|
-
* Angular surfaces this through `NavigationError`, where the recovery service
|
|
48
|
-
* recognises it. Carrying the module id across that boundary is the whole
|
|
49
|
-
* reason this error type exists.
|
|
50
|
-
*/
|
|
51
|
-
class ChunkLoadFailure extends Error {
|
|
52
|
-
moduleId;
|
|
53
|
-
cause;
|
|
54
|
-
attempts;
|
|
55
|
-
name = 'ChunkLoadFailure';
|
|
56
|
-
constructor(moduleId, cause, attempts) {
|
|
57
|
-
super(`[skew] failed to load ${moduleId ? `"${moduleId}"` : 'a lazy chunk'} ` +
|
|
58
|
-
`after ${attempts} attempt${attempts === 1 ? '' : 's'}`);
|
|
59
|
-
this.moduleId = moduleId;
|
|
60
|
-
this.cause = cause;
|
|
61
|
-
this.attempts = attempts;
|
|
62
|
-
}
|
|
23
|
+
lazyDefaults.retryAttempts = 1;
|
|
24
|
+
lazyDefaults.retryDelayMs = 250;
|
|
63
25
|
}
|
|
26
|
+
var ChunkLoadFailure = class extends Error {
|
|
27
|
+
moduleId;
|
|
28
|
+
cause;
|
|
29
|
+
attempts;
|
|
30
|
+
name = "ChunkLoadFailure";
|
|
31
|
+
constructor(moduleId, cause, attempts) {
|
|
32
|
+
super(`[skew] failed to load ${moduleId ? `"${moduleId}"` : "a lazy chunk"} after ${attempts} attempt${attempts === 1 ? "" : "s"}`);
|
|
33
|
+
this.moduleId = moduleId;
|
|
34
|
+
this.cause = cause;
|
|
35
|
+
this.attempts = attempts;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
64
38
|
function isChunkLoadFailure(value) {
|
|
65
|
-
|
|
39
|
+
return value instanceof ChunkLoadFailure;
|
|
66
40
|
}
|
|
67
|
-
/**
|
|
68
|
-
* Heuristic for "this looks like a module that failed to load" rather than an
|
|
69
|
-
* error thrown *by* the module once it ran.
|
|
70
|
-
*
|
|
71
|
-
* Deliberately broad: bundlers and browsers disagree on the shape here
|
|
72
|
-
* (`ChunkLoadError`, a bare `TypeError`, a CSP violation), and a false positive
|
|
73
|
-
* costs one wasted retry while a false negative costs the user their session.
|
|
74
|
-
*/
|
|
75
41
|
function looksLikeChunkError(error) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return (name === 'ChunkLoadError' ||
|
|
81
|
-
/loading (chunk|css chunk)/i.test(message) ||
|
|
82
|
-
/failed to fetch dynamically imported module/i.test(message) ||
|
|
83
|
-
/error loading dynamically imported module/i.test(message) ||
|
|
84
|
-
/importing a module script failed/i.test(message));
|
|
42
|
+
if (!error) return false;
|
|
43
|
+
const name = error.name ?? "";
|
|
44
|
+
const message = error.message ?? "";
|
|
45
|
+
return name === "ChunkLoadError" || /loading (chunk|css chunk)/i.test(message) || /failed to fetch dynamically imported module/i.test(message) || /error loading dynamically imported module/i.test(message) || /importing a module script failed/i.test(message);
|
|
85
46
|
}
|
|
86
47
|
function lazy(moduleIdOrLoader, loaderOrOptions, maybeOptions) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
catch (error) {
|
|
108
|
-
lastError = error;
|
|
109
|
-
// An error thrown by module *evaluation* is a bug in the module, not a
|
|
110
|
-
// transport failure. Retrying it just runs the bug again.
|
|
111
|
-
if (!looksLikeChunkError(error))
|
|
112
|
-
throw error;
|
|
113
|
-
if (attempt < attempts && delay > 0) {
|
|
114
|
-
await new Promise((resolve) => setTimeout(resolve, delay * attempt));
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
throw new ChunkLoadFailure(moduleId, lastError, attempts);
|
|
119
|
-
};
|
|
48
|
+
const hasId = typeof moduleIdOrLoader === "string";
|
|
49
|
+
const moduleId = hasId ? moduleIdOrLoader : void 0;
|
|
50
|
+
const loader = hasId ? loaderOrOptions : moduleIdOrLoader;
|
|
51
|
+
const options = (hasId ? maybeOptions : loaderOrOptions) ?? {};
|
|
52
|
+
if (typeof loader !== "function") throw new TypeError("[skew] lazy() requires a loader function");
|
|
53
|
+
return async () => {
|
|
54
|
+
if (isSkewDisabled()) return loader();
|
|
55
|
+
const attempts = Math.max(0, options.retryAttempts ?? lazyDefaults.retryAttempts) + 1;
|
|
56
|
+
const delay = options.retryDelayMs ?? lazyDefaults.retryDelayMs;
|
|
57
|
+
let lastError;
|
|
58
|
+
for (let attempt = 1; attempt <= attempts; attempt++) try {
|
|
59
|
+
return await loader();
|
|
60
|
+
} catch (error) {
|
|
61
|
+
lastError = error;
|
|
62
|
+
if (!looksLikeChunkError(error)) throw error;
|
|
63
|
+
if (attempt < attempts && delay > 0) await new Promise((resolve) => setTimeout(resolve, delay * attempt));
|
|
64
|
+
}
|
|
65
|
+
throw new ChunkLoadFailure(moduleId, lastError, attempts);
|
|
66
|
+
};
|
|
120
67
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: UnsavedWorkRegistry, decorators: [{
|
|
168
|
-
type: Injectable,
|
|
169
|
-
args: [{ providedIn: 'root' }]
|
|
170
|
-
}] });
|
|
171
|
-
/**
|
|
172
|
-
* Declares that this component holds unsaved work, so automatic recovery will
|
|
173
|
-
* not discard it. Unregisters automatically on destroy.
|
|
174
|
-
*
|
|
175
|
-
* ```ts
|
|
176
|
-
* export class BulletinEditor {
|
|
177
|
-
* protected readonly form = …;
|
|
178
|
-
* constructor() {
|
|
179
|
-
* trackUnsavedWork(() => this.form.dirty);
|
|
180
|
-
* }
|
|
181
|
-
* }
|
|
182
|
-
* ```
|
|
183
|
-
*
|
|
184
|
-
* Must be called in an injection context.
|
|
185
|
-
*/
|
|
68
|
+
var UnsavedWorkRegistry = class UnsavedWorkRegistry {
|
|
69
|
+
sources = signal([], ...ngDevMode ? [{ debugName: "sources" }] : /* istanbul ignore next */ []);
|
|
70
|
+
register(isDirty) {
|
|
71
|
+
this.sources.update((current) => [...current, isDirty]);
|
|
72
|
+
return () => {
|
|
73
|
+
this.sources.update((current) => current.filter((entry) => entry !== isDirty));
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
isDirty() {
|
|
77
|
+
return this.sources().some((isDirty) => {
|
|
78
|
+
try {
|
|
79
|
+
return isDirty();
|
|
80
|
+
} catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
get size() {
|
|
86
|
+
return this.sources().length;
|
|
87
|
+
}
|
|
88
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
89
|
+
minVersion: "12.0.0",
|
|
90
|
+
version: "22.2.3",
|
|
91
|
+
ngImport: i0,
|
|
92
|
+
type: UnsavedWorkRegistry,
|
|
93
|
+
deps: [],
|
|
94
|
+
target: i0.ɵɵFactoryTarget.Injectable
|
|
95
|
+
});
|
|
96
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({
|
|
97
|
+
minVersion: "12.0.0",
|
|
98
|
+
version: "22.2.3",
|
|
99
|
+
ngImport: i0,
|
|
100
|
+
type: UnsavedWorkRegistry,
|
|
101
|
+
providedIn: "root"
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
105
|
+
minVersion: "12.0.0",
|
|
106
|
+
version: "22.2.3",
|
|
107
|
+
ngImport: i0,
|
|
108
|
+
type: UnsavedWorkRegistry,
|
|
109
|
+
decorators: [{
|
|
110
|
+
type: Injectable,
|
|
111
|
+
args: [{ providedIn: "root" }]
|
|
112
|
+
}]
|
|
113
|
+
});
|
|
186
114
|
function trackUnsavedWork(isDirty) {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
115
|
+
const registry = inject(UnsavedWorkRegistry);
|
|
116
|
+
const destroyRef = inject(DestroyRef);
|
|
117
|
+
const dispose = registry.register(isDirty);
|
|
118
|
+
destroyRef.onDestroy(dispose);
|
|
191
119
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
*/
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
// --- environment access, all browser-guarded ---------------------------
|
|
358
|
-
isOnline() {
|
|
359
|
-
if (!this.isBrowser)
|
|
360
|
-
return true;
|
|
361
|
-
const nav = this.document.defaultView?.navigator;
|
|
362
|
-
return nav?.onLine ?? true;
|
|
363
|
-
}
|
|
364
|
-
assign(url) {
|
|
365
|
-
if (!this.isBrowser)
|
|
366
|
-
return;
|
|
367
|
-
this.document.defaultView?.location.assign(url);
|
|
368
|
-
}
|
|
369
|
-
reload() {
|
|
370
|
-
if (!this.isBrowser)
|
|
371
|
-
return;
|
|
372
|
-
this.document.defaultView?.location.reload();
|
|
373
|
-
}
|
|
374
|
-
/**
|
|
375
|
-
* Recoveries already attempted, kept in `sessionStorage` so it survives the
|
|
376
|
-
* very reload it is counting. Without this, an origin serving a stale entry
|
|
377
|
-
* document turns one failure into an infinite reload loop.
|
|
378
|
-
*/
|
|
379
|
-
recoveryCount() {
|
|
380
|
-
if (!this.isBrowser)
|
|
381
|
-
return 0;
|
|
382
|
-
try {
|
|
383
|
-
const raw = this.document.defaultView?.sessionStorage.getItem(LOOP_GUARD_KEY);
|
|
384
|
-
const parsed = raw ? JSON.parse(raw) : null;
|
|
385
|
-
// Scoped to a build: a genuinely new deployment gets a fresh budget.
|
|
386
|
-
return parsed?.buildId === this.options.identity.buildId ? (parsed.count ?? 0) : 0;
|
|
387
|
-
}
|
|
388
|
-
catch {
|
|
389
|
-
return 0;
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
countRecovery() {
|
|
393
|
-
if (!this.isBrowser)
|
|
394
|
-
return;
|
|
395
|
-
try {
|
|
396
|
-
this.document.defaultView?.sessionStorage.setItem(LOOP_GUARD_KEY, JSON.stringify({ buildId: this.options.identity.buildId, count: this.recoveryCount() + 1 }));
|
|
397
|
-
}
|
|
398
|
-
catch {
|
|
399
|
-
// Private mode. The manifest probe remains as the precise loop guard.
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: SkewRecoveryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
403
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: SkewRecoveryService, providedIn: 'root' });
|
|
404
|
-
}
|
|
405
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: SkewRecoveryService, decorators: [{
|
|
406
|
-
type: Injectable,
|
|
407
|
-
args: [{ providedIn: 'root' }]
|
|
408
|
-
}], ctorParameters: () => [] });
|
|
409
|
-
|
|
410
|
-
/**
|
|
411
|
-
* Enables skew recovery.
|
|
412
|
-
*
|
|
413
|
-
* ```ts
|
|
414
|
-
* bootstrapApplication(App, {
|
|
415
|
-
* providers: [
|
|
416
|
-
* provideRouter(routes),
|
|
417
|
-
* provideSkewRecovery({
|
|
418
|
-
* identity: BUILD_IDENTITY, // from @braidlabs/build
|
|
419
|
-
* manifestUrl: '/skew-manifest.json',
|
|
420
|
-
* }),
|
|
421
|
-
* ],
|
|
422
|
-
* });
|
|
423
|
-
* ```
|
|
424
|
-
*
|
|
425
|
-
* Deliberately *not* a `RouterFeature`: Angular's feature factory is internal,
|
|
426
|
-
* so third parties cannot construct one. This composes alongside
|
|
427
|
-
* `provideRouter` instead, and works with any router configuration.
|
|
428
|
-
*
|
|
429
|
-
* The service is instantiated eagerly — it subscribes to router events, and a
|
|
430
|
-
* lazily-created recovery service would miss the very first failure.
|
|
431
|
-
*/
|
|
120
|
+
const LOOP_GUARD_KEY = "skew:recoveries";
|
|
121
|
+
var SkewRecoveryService = class SkewRecoveryService {
|
|
122
|
+
options = inject(SKEW_RECOVERY_OPTIONS);
|
|
123
|
+
router = inject(Router);
|
|
124
|
+
document = inject(DOCUMENT);
|
|
125
|
+
unsavedWork = inject(UnsavedWorkRegistry);
|
|
126
|
+
isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
|
|
127
|
+
probe;
|
|
128
|
+
pendingSignal = signal(null, ...ngDevMode ? [{ debugName: "pendingSignal" }] : /* istanbul ignore next */ []);
|
|
129
|
+
statusSignal = signal(null, ...ngDevMode ? [{ debugName: "statusSignal" }] : /* istanbul ignore next */ []);
|
|
130
|
+
pending = this.pendingSignal.asReadonly();
|
|
131
|
+
status = this.statusSignal.asReadonly();
|
|
132
|
+
updateAvailable = computed(() => this.statusSignal()?.kind === "staleClient", ...ngDevMode ? [{ debugName: "updateAvailable" }] : /* istanbul ignore next */ []);
|
|
133
|
+
constructor() {
|
|
134
|
+
this.probe = this.options.manifestUrl ? createVersionProbe({
|
|
135
|
+
identity: this.options.identity,
|
|
136
|
+
manifestUrl: this.options.manifestUrl
|
|
137
|
+
}) : null;
|
|
138
|
+
if (this.isBrowser) this.listen();
|
|
139
|
+
}
|
|
140
|
+
listen() {
|
|
141
|
+
this.router.events.subscribe((event) => {
|
|
142
|
+
if (event instanceof NavigationError && isChunkLoadFailure(event.error)) {
|
|
143
|
+
if (isSkewDisabled()) return;
|
|
144
|
+
this.handle(event.error, event.url);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
async check() {
|
|
149
|
+
if (!this.probe) return null;
|
|
150
|
+
const status = await this.probe.check();
|
|
151
|
+
this.statusSignal.set(status);
|
|
152
|
+
return status;
|
|
153
|
+
}
|
|
154
|
+
recover() {
|
|
155
|
+
const pending = this.pendingSignal();
|
|
156
|
+
if (!pending) return;
|
|
157
|
+
this.pendingSignal.set(null);
|
|
158
|
+
this.assign(pending.targetUrl);
|
|
159
|
+
}
|
|
160
|
+
dismiss() {
|
|
161
|
+
this.pendingSignal.set(null);
|
|
162
|
+
}
|
|
163
|
+
async handle(failure, targetUrl) {
|
|
164
|
+
const context = await this.classify(failure, targetUrl);
|
|
165
|
+
const action = await this.chooseAction(context);
|
|
166
|
+
this.dispatch(action, context);
|
|
167
|
+
}
|
|
168
|
+
async classify(failure, targetUrl) {
|
|
169
|
+
const status = await this.check();
|
|
170
|
+
const serverBuildId = status?.kind === "current" ? status.buildId : status && "remote" in status ? status.remote : void 0;
|
|
171
|
+
const manifest = this.probe?.lastManifest() ?? null;
|
|
172
|
+
const moduleStillExists = failure.moduleId === void 0 || manifest === null || !moduleWasRemoved(manifest, failure.moduleId);
|
|
173
|
+
return {
|
|
174
|
+
targetUrl,
|
|
175
|
+
currentUrl: this.router.url,
|
|
176
|
+
moduleId: failure.moduleId,
|
|
177
|
+
error: failure,
|
|
178
|
+
attempt: this.recoveryCount(),
|
|
179
|
+
isOnline: this.isOnline(),
|
|
180
|
+
clientBuildId: this.options.identity.buildId,
|
|
181
|
+
serverBuildId,
|
|
182
|
+
moduleStillExists,
|
|
183
|
+
entryDocumentStale: status?.kind === "staleOrigin",
|
|
184
|
+
newAssetsReachable: status?.kind === "staleClient",
|
|
185
|
+
hasUnsavedWork: this.options.respectUnsavedWork && this.unsavedWork.isDirty()
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
async chooseAction(context) {
|
|
189
|
+
if (!context.isOnline) return "notify";
|
|
190
|
+
if (context.entryDocumentStale) return "notify";
|
|
191
|
+
if (context.attempt >= this.options.maxRecoveries) return "notify";
|
|
192
|
+
if (context.hasUnsavedWork) return "notify";
|
|
193
|
+
if (!context.moduleStillExists) return "redirect-to-fallback";
|
|
194
|
+
const strategy = this.options.onStaleChunk;
|
|
195
|
+
if (typeof strategy === "function") try {
|
|
196
|
+
return await strategy(context);
|
|
197
|
+
} catch {
|
|
198
|
+
return "notify";
|
|
199
|
+
}
|
|
200
|
+
return strategy;
|
|
201
|
+
}
|
|
202
|
+
dispatch(action, context) {
|
|
203
|
+
switch (action) {
|
|
204
|
+
case "reload-at-target":
|
|
205
|
+
this.countRecovery();
|
|
206
|
+
this.assign(context.targetUrl);
|
|
207
|
+
return;
|
|
208
|
+
case "reload-in-place":
|
|
209
|
+
this.countRecovery();
|
|
210
|
+
this.reload();
|
|
211
|
+
return;
|
|
212
|
+
case "redirect-to-fallback":
|
|
213
|
+
this.router.navigateByUrl(this.options.fallbackRoute);
|
|
214
|
+
return;
|
|
215
|
+
case "ignore": return;
|
|
216
|
+
case "notify":
|
|
217
|
+
this.pendingSignal.set({
|
|
218
|
+
targetUrl: context.targetUrl,
|
|
219
|
+
moduleId: context.moduleId,
|
|
220
|
+
serverBuildId: context.serverBuildId,
|
|
221
|
+
reason: !context.isOnline ? "offline" : context.entryDocumentStale ? "loop-detected" : context.attempt >= this.options.maxRecoveries ? "exhausted" : "notify",
|
|
222
|
+
context
|
|
223
|
+
});
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
isOnline() {
|
|
228
|
+
if (!this.isBrowser) return true;
|
|
229
|
+
return (this.document.defaultView?.navigator)?.onLine ?? true;
|
|
230
|
+
}
|
|
231
|
+
assign(url) {
|
|
232
|
+
if (!this.isBrowser) return;
|
|
233
|
+
this.document.defaultView?.location.assign(url);
|
|
234
|
+
}
|
|
235
|
+
reload() {
|
|
236
|
+
if (!this.isBrowser) return;
|
|
237
|
+
this.document.defaultView?.location.reload();
|
|
238
|
+
}
|
|
239
|
+
recoveryCount() {
|
|
240
|
+
if (!this.isBrowser) return 0;
|
|
241
|
+
try {
|
|
242
|
+
const raw = this.document.defaultView?.sessionStorage.getItem(LOOP_GUARD_KEY);
|
|
243
|
+
const parsed = raw ? JSON.parse(raw) : null;
|
|
244
|
+
return parsed?.buildId === this.options.identity.buildId ? parsed.count ?? 0 : 0;
|
|
245
|
+
} catch {
|
|
246
|
+
return 0;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
countRecovery() {
|
|
250
|
+
if (!this.isBrowser) return;
|
|
251
|
+
try {
|
|
252
|
+
this.document.defaultView?.sessionStorage.setItem(LOOP_GUARD_KEY, JSON.stringify({
|
|
253
|
+
buildId: this.options.identity.buildId,
|
|
254
|
+
count: this.recoveryCount() + 1
|
|
255
|
+
}));
|
|
256
|
+
} catch {}
|
|
257
|
+
}
|
|
258
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
259
|
+
minVersion: "12.0.0",
|
|
260
|
+
version: "22.2.3",
|
|
261
|
+
ngImport: i0,
|
|
262
|
+
type: SkewRecoveryService,
|
|
263
|
+
deps: [],
|
|
264
|
+
target: i0.ɵɵFactoryTarget.Injectable
|
|
265
|
+
});
|
|
266
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({
|
|
267
|
+
minVersion: "12.0.0",
|
|
268
|
+
version: "22.2.3",
|
|
269
|
+
ngImport: i0,
|
|
270
|
+
type: SkewRecoveryService,
|
|
271
|
+
providedIn: "root"
|
|
272
|
+
});
|
|
273
|
+
};
|
|
274
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
275
|
+
minVersion: "12.0.0",
|
|
276
|
+
version: "22.2.3",
|
|
277
|
+
ngImport: i0,
|
|
278
|
+
type: SkewRecoveryService,
|
|
279
|
+
decorators: [{
|
|
280
|
+
type: Injectable,
|
|
281
|
+
args: [{ providedIn: "root" }]
|
|
282
|
+
}],
|
|
283
|
+
ctorParameters: () => []
|
|
284
|
+
});
|
|
432
285
|
function provideSkewRecovery(options) {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
inject(SkewRecoveryService);
|
|
443
|
-
}),
|
|
444
|
-
]);
|
|
286
|
+
const resolved = resolveOptions(options);
|
|
287
|
+
return makeEnvironmentProviders([{
|
|
288
|
+
provide: SKEW_RECOVERY_OPTIONS,
|
|
289
|
+
useValue: resolved
|
|
290
|
+
}, provideEnvironmentInitializer(() => {
|
|
291
|
+
lazyDefaults.retryAttempts = resolved.retryAttempts;
|
|
292
|
+
lazyDefaults.retryDelayMs = resolved.retryDelayMs;
|
|
293
|
+
inject(SkewRecoveryService);
|
|
294
|
+
})]);
|
|
445
295
|
}
|
|
446
|
-
|
|
447
|
-
/**
|
|
448
|
-
* ⚠️ NOT PUBLIC API. Undocumented on purpose. Do not use in an application.
|
|
449
|
-
*
|
|
450
|
-
* Angular-shaped wrapper around `setSkewDisabled()` from `@braidlabs/skew`. Read the
|
|
451
|
-
* module comment in `@braidlabs/skew`'s `disabled.ts` first — it explains what this
|
|
452
|
-
* turns off, why it exists, and why it is deliberately absent from the README,
|
|
453
|
-
* the package docs, and every example.
|
|
454
|
-
*
|
|
455
|
-
* ```ts
|
|
456
|
-
* // demo apps only
|
|
457
|
-
* providers: [provideSkewRecovery({ … }), provideSkewDisabled()]
|
|
458
|
-
* ```
|
|
459
|
-
*
|
|
460
|
-
* Two things worth knowing before using it anyway:
|
|
461
|
-
*
|
|
462
|
-
* **It is not scoped to this injector.** The flag lives in a module-level
|
|
463
|
-
* variable in `@braidlabs/skew`, because core is framework-agnostic and has no
|
|
464
|
-
* injector to read from. Calling this affects every `@braidlabs` package sharing
|
|
465
|
-
* that instance of core — including any other Angular application on the page.
|
|
466
|
-
* It is `provide*`-shaped for familiarity, not because it is scoped.
|
|
467
|
-
*
|
|
468
|
-
* **It applies at bootstrap.** For a control that flips at runtime — which is
|
|
469
|
-
* what a before/after demo actually wants — call `setSkewDisabled()` directly.
|
|
470
|
-
* Every check reads the flag at the moment it runs, so a later toggle takes
|
|
471
|
-
* effect on the next read, navigation or write with no reload.
|
|
472
|
-
*
|
|
473
|
-
* This lives in `@braidlabs/angular-router` for want of a shared Angular package;
|
|
474
|
-
* it belongs in `@braidlabs/angular-core` once that lands, since it governs all of
|
|
475
|
-
* them and nothing here is router-specific.
|
|
476
|
-
*
|
|
477
|
-
* @internal
|
|
478
|
-
*/
|
|
479
296
|
function provideSkewDisabled(disabled = true) {
|
|
480
|
-
|
|
481
|
-
provideEnvironmentInitializer(() => setSkewDisabled(disabled)),
|
|
482
|
-
]);
|
|
297
|
+
return makeEnvironmentProviders([provideEnvironmentInitializer(() => setSkewDisabled(disabled))]);
|
|
483
298
|
}
|
|
484
|
-
|
|
485
|
-
/**
|
|
486
|
-
* Generated bundle index. Do not edit.
|
|
487
|
-
*/
|
|
488
|
-
|
|
489
299
|
export { ChunkLoadFailure, SKEW_RECOVERY_OPTIONS, SkewRecoveryService, UnsavedWorkRegistry, isChunkLoadFailure, lazy, lazyDefaults, looksLikeChunkError, provideSkewDisabled, provideSkewRecovery, resetLazyDefaults, resolveOptions, trackUnsavedWork };
|
|
490
|
-
|
|
300
|
+
|
|
301
|
+
//# sourceMappingURL=braidlabs-angular-router.mjs.map
|