From 9d262928dc067e224d17c1f8066d88e61b23af77 Mon Sep 17 00:00:00 2001 From: b84330808 Date: Sat, 7 Nov 2026 22:05:29 +0900 Subject: [PATCH] fix(core): create a lazy plugin implementation only once When a plugin registers a lazy implementation such as `web: () => import('./web').then((m) => new m.MyWeb())`, every call made before the first load settled ran the loader again. Calling addListener() twice without awaiting created two instances, and only the last one was kept, so listeners added to the others never fired. Share the pending load between those calls. A load that fails is still retried on the next call. Fixes #6938 --- core/src/runtime.ts | 17 ++++++++++++- core/src/tests/plugin.spec.ts | 46 +++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/core/src/runtime.ts b/core/src/runtime.ts index 2b025f2459..90570f43d5 100644 --- a/core/src/runtime.ts +++ b/core/src/runtime.ts @@ -58,8 +58,23 @@ export const createCapacitor = (win: WindowCapacitor): CapacitorInstance => { const platform = getPlatform(); const pluginHeader = getPluginHeader(pluginName); let jsImplementation: any; + let jsImplementationPromise: Promise | undefined; + + const loadPluginImplementation = (): Promise => { + // Share one load between calls made before it settles, so a lazy + // implementation such as `web: () => import('./web').then(...)` is + // only created once. A failed load is retried on the next call. + if (!jsImplementationPromise) { + jsImplementationPromise = resolvePluginImplementation().catch((err) => { + jsImplementationPromise = undefined; + throw err; + }); + } + + return jsImplementationPromise; + }; - const loadPluginImplementation = async (): Promise => { + const resolvePluginImplementation = async (): Promise => { if (!jsImplementation && platform in jsImplementations) { jsImplementation = typeof jsImplementations[platform] === 'function' diff --git a/core/src/tests/plugin.spec.ts b/core/src/tests/plugin.spec.ts index 570134a023..1ce3545763 100644 --- a/core/src/tests/plugin.spec.ts +++ b/core/src/tests/plugin.spec.ts @@ -329,6 +329,52 @@ describe('plugin', () => { expect(rtn).toBeUndefined(); }); + it('calls made before a lazy web implementation loads share one instance', async () => { + let created = 0; + const AwesomeWeb = class extends WebPlugin { + constructor() { + super(); + created++; + } + async fire() { + this.notifyListeners('change', { value: 88 }); + } + }; + cap = initCapacitorGlobal(win); + const Awesome = cap.registerPlugin('Awesome', { + web: async () => new AwesomeWeb(), + }); + + const first = jest.fn(); + const second = jest.fn(); + // neither listener is awaited before the next call + const handles = [Awesome.addListener('change', first), Awesome.addListener('change', second)]; + await Promise.all(handles); + await Awesome.fire(); + + expect(created).toBe(1); + expect(first).toHaveBeenCalledWith({ value: 88 }); + expect(second).toHaveBeenCalledWith({ value: 88 }); + }); + + it('retries a lazy implementation that failed to load', async () => { + let attempts = 0; + cap = initCapacitorGlobal(win); + const Awesome = cap.registerPlugin('Awesome', { + web: async () => { + attempts++; + if (attempts === 1) { + throw 'unable to load module'; + } + return { mph: async () => 88 }; + }, + }); + + await expect(Awesome.mph()).rejects.toBe('unable to load module'); + expect(await Awesome.mph()).toBe(88); + expect(attempts).toBe(2); + }); + const mockAndroidBridge = () => { win.androidBridge = { postMessage: () => {