-
-
Notifications
You must be signed in to change notification settings - Fork 852
fix(runtime): retry lazy component load after a failed dynamic import #6772
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
6514fa6
12b57d2
deeab34
9db331e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -118,6 +118,23 @@ export const connectedCallback = (elm: d.HostElement) => { | |||||||||||||||||||||||||||||||||
| // fire off connectedCallback() on component instance | ||||||||||||||||||||||||||||||||||
| if (hostRef?.$lazyInstance$) { | ||||||||||||||||||||||||||||||||||
| fireConnectedCallback(hostRef.$lazyInstance$, elm); | ||||||||||||||||||||||||||||||||||
| } else if (hostRef.$flags$ & HOST_FLAGS.hasFailedLoad) { | ||||||||||||||||||||||||||||||||||
| // A previous initialization attempt for this host element failed | ||||||||||||||||||||||||||||||||||
| // (e.g. the lazy bundle's dynamic import() was rejected) -- see | ||||||||||||||||||||||||||||||||||
| // `initialize-component.ts`. Retry now that we're reconnecting, | ||||||||||||||||||||||||||||||||||
| // rather than leaving the element permanently un-upgraded for the | ||||||||||||||||||||||||||||||||||
| // rest of the page's lifetime. | ||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||
| // This must key off the dedicated `hasFailedLoad` flag rather than | ||||||||||||||||||||||||||||||||||
| // `hasInitializedComponent` being unset: that flag is also | ||||||||||||||||||||||||||||||||||
| // (transiently) unset while an initialization attempt is merely | ||||||||||||||||||||||||||||||||||
| // queued/in-flight and hasn't failed at all -- e.g. behind | ||||||||||||||||||||||||||||||||||
| // `nextTick`, or when `connectedCallback` fires again for the same | ||||||||||||||||||||||||||||||||||
| // element before that queued attempt has run, which Stencil's own | ||||||||||||||||||||||||||||||||||
| // server-side hydration deliberately does (see | ||||||||||||||||||||||||||||||||||
| // `serverSideConnected()` in `update-component.ts`). Retrying in | ||||||||||||||||||||||||||||||||||
| // that case double-initializes the component. | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+122
to
+136
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. again - very verbose and repeated in constants.ts
Suggested change
|
||||||||||||||||||||||||||||||||||
| initializeComponent(elm, hostRef, cmpMeta); | ||||||||||||||||||||||||||||||||||
| } else if (hostRef?.$onReadyPromise$) { | ||||||||||||||||||||||||||||||||||
| hostRef.$onReadyPromise$.then(() => fireConnectedCallback(hostRef.$lazyInstance$, elm)); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -34,6 +34,8 @@ export const initializeComponent = async ( | |||||||||||||||||||||||
| if ((hostRef.$flags$ & HOST_FLAGS.hasInitializedComponent) === 0) { | ||||||||||||||||||||||||
| // Let the runtime know that the component has been initialized | ||||||||||||||||||||||||
| hostRef.$flags$ |= HOST_FLAGS.hasInitializedComponent; | ||||||||||||||||||||||||
| // Starting a fresh attempt clears any failure recorded by a previous one. | ||||||||||||||||||||||||
| hostRef.$flags$ &= ~HOST_FLAGS.hasFailedLoad; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const bundleId = cmpMeta.$lazyBundleId$; | ||||||||||||||||||||||||
| if (BUILD.lazyLoad && bundleId) { | ||||||||||||||||||||||||
|
|
@@ -53,6 +55,18 @@ export const initializeComponent = async ( | |||||||||||||||||||||||
| Cstr = CstrImport as d.ComponentConstructor | undefined; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| if (!Cstr) { | ||||||||||||||||||||||||
| // The lazy bundle failed to load (e.g. a dropped network request). | ||||||||||||||||||||||||
| // Clear the "initialized" flag and mark the load as failed so a | ||||||||||||||||||||||||
| // future `connectedCallback` (see `connected-callback.ts`) is able | ||||||||||||||||||||||||
| // to retry loading this component instead of leaving the host | ||||||||||||||||||||||||
| // element permanently un-upgraded. `hasFailedLoad` (rather than | ||||||||||||||||||||||||
| // just the absence of `hasInitializedComponent`) is what gates the | ||||||||||||||||||||||||
| // retry, since `hasInitializedComponent` is also unset while an | ||||||||||||||||||||||||
| // attempt is merely queued/in-flight (e.g. behind `nextTick`, or | ||||||||||||||||||||||||
| // between repeated `connectedCallback` invocations during | ||||||||||||||||||||||||
| // server-side hydration) -- neither of which is a failure. | ||||||||||||||||||||||||
|
Comment on lines
+58
to
+67
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. again - unnecessary - I think it's clear from context :)
Suggested change
|
||||||||||||||||||||||||
| hostRef.$flags$ &= ~HOST_FLAGS.hasInitializedComponent; | ||||||||||||||||||||||||
| hostRef.$flags$ |= HOST_FLAGS.hasFailedLoad; | ||||||||||||||||||||||||
| throw new Error(`Constructor for "${cmpMeta.$tagName$}#${hostRef.$modeName$}" was not found`); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| if (BUILD.member && !Cstr.isProxied) { | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there doesn't appear to be any coverage here on the actual network-facing fix(?) - the |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,74 @@ | ||||||||||||||
| import { flushAll, flushLoadModule, getHostRef, registerInstance, registerModule, win } from '@platform'; | ||||||||||||||
|
|
||||||||||||||
| import { LazyBundlesRuntimeData } from '../../internal'; | ||||||||||||||
| import { HOST_FLAGS } from '../../utils'; | ||||||||||||||
| import { bootstrapLazy } from '../bootstrap-lazy'; | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * Regression tests for a bug where a host element would be permanently | ||||||||||||||
| * "bricked" (never rendered, no lifecycle callbacks) if its lazy bundle | ||||||||||||||
| * failed to load a single time (e.g. a dropped network request). See: | ||||||||||||||
| * https://github.com/stenciljs/core/issues/6771 | ||||||||||||||
| */ | ||||||||||||||
|
Comment on lines
+7
to
+12
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. unnecessary :)
Suggested change
|
||||||||||||||
| describe('lazy-load failure recovery', () => { | ||||||||||||||
| const bundleId = 'cmp-retry-bundle'; | ||||||||||||||
| let lazyBundles: LazyBundlesRuntimeData; | ||||||||||||||
|
|
||||||||||||||
| beforeEach(() => { | ||||||||||||||
| lazyBundles = [[bundleId, [[0, 'cmp-retry', {}]]]]; | ||||||||||||||
| }); | ||||||||||||||
|
|
||||||||||||||
| it('clears HOST_FLAGS.hasInitializedComponent when the lazy module fails to load', async () => { | ||||||||||||||
| bootstrapLazy(lazyBundles); | ||||||||||||||
|
|
||||||||||||||
| const elm = win.document.createElement('cmp-retry'); | ||||||||||||||
| win.document.body.appendChild(elm); | ||||||||||||||
|
|
||||||||||||||
| // No `registerModule(bundleId, ...)` call was made, so the testing | ||||||||||||||
| // platform's `loadModule()` resolves to `undefined` here -- simulating a | ||||||||||||||
| // failed dynamic import() of the real `*.entry.js` chunk. | ||||||||||||||
| await flushLoadModule(bundleId); | ||||||||||||||
| await flushAll().catch(() => { | ||||||||||||||
| /* initializeComponent's internal catch already logs/handles this */ | ||||||||||||||
| }); | ||||||||||||||
|
|
||||||||||||||
| const hostRef = getHostRef(elm); | ||||||||||||||
| expect(hostRef?.$lazyInstance$).toBeUndefined(); | ||||||||||||||
| expect((hostRef?.$flags$ ?? 0) & HOST_FLAGS.hasInitializedComponent).toBe(0); | ||||||||||||||
| }); | ||||||||||||||
|
|
||||||||||||||
| it('retries initialization when the host element is reconnected after a failed load', async () => { | ||||||||||||||
| bootstrapLazy(lazyBundles); | ||||||||||||||
|
|
||||||||||||||
| const elm = win.document.createElement('cmp-retry'); | ||||||||||||||
| win.document.body.appendChild(elm); | ||||||||||||||
|
|
||||||||||||||
| // First attempt fails (module never registered). | ||||||||||||||
| await flushLoadModule(bundleId); | ||||||||||||||
| await flushAll().catch(() => {}); | ||||||||||||||
|
|
||||||||||||||
| expect(getHostRef(elm)?.$lazyInstance$).toBeUndefined(); | ||||||||||||||
|
|
||||||||||||||
| // "Network recovers": the module becomes available, then the element is | ||||||||||||||
| // reconnected (disconnect + reconnect is the retry trigger). | ||||||||||||||
| class CmpRetry { | ||||||||||||||
| constructor(hostRef: any) { | ||||||||||||||
| registerInstance(this, hostRef); | ||||||||||||||
| } | ||||||||||||||
| render() { | ||||||||||||||
| return null; | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| registerModule(bundleId, CmpRetry as any); | ||||||||||||||
|
|
||||||||||||||
| elm.remove(); | ||||||||||||||
| win.document.body.appendChild(elm); | ||||||||||||||
|
|
||||||||||||||
| await flushLoadModule(bundleId); | ||||||||||||||
| await flushAll().catch(() => {}); | ||||||||||||||
|
|
||||||||||||||
| const hostRef = getHostRef(elm); | ||||||||||||||
| expect(hostRef?.$lazyInstance$).toBeInstanceOf(CmpRetry); | ||||||||||||||
| expect((hostRef?.$flags$ ?? 0) & HOST_FLAGS.hasInitializedComponent).toBe(HOST_FLAGS.hasInitializedComponent); | ||||||||||||||
| }); | ||||||||||||||
| }); | ||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -56,10 +56,18 @@ export const enum HOST_FLAGS { | |||||||||||||||||||||||||
| isWatchReady = 1 << 7, | ||||||||||||||||||||||||||
| isListenReady = 1 << 8, | ||||||||||||||||||||||||||
| needsRerender = 1 << 9, | ||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||
| * Set when a lazy component's dynamic `import()` fails to resolve a | ||||||||||||||||||||||||||
| * constructor. Distinct from `hasInitializedComponent` being unset, which | ||||||||||||||||||||||||||
| * is also (transiently) true while an initialization attempt is merely | ||||||||||||||||||||||||||
| * queued/in-flight (e.g. behind `nextTick`) and hasn't failed at all. | ||||||||||||||||||||||||||
| * Only this flag should gate a `connectedCallback` retry. | ||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||
|
Comment on lines
+59
to
+65
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||
| hasFailedLoad = 1 << 10, | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // DEV ONLY | ||||||||||||||||||||||||||
| devOnRender = 1 << 10, | ||||||||||||||||||||||||||
| devOnDidLoad = 1 << 11, | ||||||||||||||||||||||||||
| devOnRender = 1 << 11, | ||||||||||||||||||||||||||
| devOnDidLoad = 1 << 12, | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
quite verbose :)