Repository navigation
fix: resolve all waiters for plugin loads #2955
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
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 |
|---|---|---|
|
|
@@ -66,6 +66,7 @@ import { | |
| import notificationManager from "lib/notificationManager"; | ||
| import openFolder, { addedFolder } from "lib/openFolder"; | ||
| import orientation from "lib/orientation"; | ||
| import PluginWaiters from "lib/pluginWaiters"; | ||
| import projects from "lib/projects"; | ||
| import selectionMenu from "lib/selectionMenu"; | ||
| import appSettings from "lib/settings"; | ||
|
|
@@ -96,7 +97,7 @@ class Acode { | |
| #pluginUnmount = {}; | ||
| // Registered formatter implementations (populated by plugins) | ||
| #formatter = []; | ||
| #pluginWatchers = {}; | ||
| #pluginWatchers = new PluginWaiters(); | ||
|
|
||
| /** | ||
| * Clear a plugin's broken mark (so it can be retried) | ||
|
|
@@ -700,32 +701,18 @@ class Acode { | |
| } | ||
|
|
||
| [onPluginLoadCallback](pluginId) { | ||
| if (this.#pluginWatchers[pluginId]) { | ||
| this.#pluginWatchers[pluginId].resolve(); | ||
| delete this.#pluginWatchers[pluginId]; | ||
| } | ||
| this.#pluginWatchers.resolve(pluginId); | ||
| } | ||
|
|
||
| [onPluginsLoadCompleteCallback]() { | ||
| for (const pluginId in this.#pluginWatchers) { | ||
| this.#pluginWatchers[pluginId].reject( | ||
| new Error(`Plugin '${pluginId}' failed to load.`), | ||
| ); | ||
| } | ||
| this.#pluginWatchers = {}; | ||
| this.#pluginWatchers.rejectAll( | ||
|
Member
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. This callback also fires at the end of the theme-only pass ( Could we only reject when |
||
| (pluginId) => new Error(`Plugin '${pluginId}' failed to load.`), | ||
| ); | ||
| } | ||
|
|
||
| waitForPlugin(pluginId) { | ||
|
Member
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. If the initial load has already completed ( Since this function is being rewritten anyway, it'd be good to reject immediately in that case. Related: with the new |
||
| return new Promise((resolve, reject) => { | ||
| if (LOADED_PLUGINS.has(pluginId)) { | ||
| return resolve(true); | ||
| } | ||
|
|
||
| this.#pluginWatchers[pluginId] = { | ||
| resolve, | ||
| reject, | ||
| }; | ||
| }); | ||
| if (LOADED_PLUGINS.has(pluginId)) return Promise.resolve(true); | ||
|
Member
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. Nit: the fast path resolves with |
||
| return this.#pluginWatchers.waitFor(pluginId); | ||
| } | ||
|
|
||
| get exitAppMessage() { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| export default class PluginWaiters { | ||
| #waiters = new Map(); | ||
|
|
||
| waitFor(pluginId) { | ||
| return new Promise((resolve, reject) => { | ||
| let waiters = this.#waiters.get(pluginId); | ||
| if (!waiters) { | ||
| waiters = new Set(); | ||
| this.#waiters.set(pluginId, waiters); | ||
| } | ||
| waiters.add({ resolve, reject }); | ||
|
Member
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. Suggestion: rather than a waitFor(pluginId) {
let deferred = this.#waiters.get(pluginId);
if (!deferred) {
deferred = Promise.withResolvers();
this.#waiters.set(pluginId, deferred);
}
return deferred.promise;
}
|
||
| }); | ||
| } | ||
|
|
||
| resolve(pluginId) { | ||
| const waiters = this.#waiters.get(pluginId); | ||
| if (!waiters) return; | ||
|
|
||
| this.#waiters.delete(pluginId); | ||
| for (const waiter of waiters) waiter.resolve(); | ||
| } | ||
|
|
||
| rejectAll(getError) { | ||
| for (const [pluginId, waiters] of this.#waiters) { | ||
| this.#waiters.delete(pluginId); | ||
| for (const waiter of waiters) waiter.reject(getError(pluginId)); | ||
|
Member
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.
|
||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import { expect, it } from "vitest"; | ||
|
Member
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. These tests cover |
||
| import PluginWaiters from "../../src/lib/pluginWaiters"; | ||
|
|
||
| it("resolves every caller waiting for the same plugin", async () => { | ||
| const waiters = new PluginWaiters(); | ||
| const first = waiters.waitFor("example"); | ||
| const second = waiters.waitFor("example"); | ||
|
|
||
| waiters.resolve("example"); | ||
|
|
||
| await expect(Promise.all([first, second])).resolves.toEqual([ | ||
| undefined, | ||
| undefined, | ||
| ]); | ||
| }); | ||
|
|
||
| it("rejects every pending caller with its plugin-specific error", async () => { | ||
| const waiters = new PluginWaiters(); | ||
| const first = waiters.waitFor("first"); | ||
| const second = waiters.waitFor("first"); | ||
| const third = waiters.waitFor("second"); | ||
| const pending = Promise.allSettled([first, second, third]); | ||
|
|
||
| waiters.rejectAll((pluginId) => new Error(`Plugin '${pluginId}' failed to load.`)); | ||
|
|
||
|
Member
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. Nit: this file isn't Biome-formatted (several lines exceed the 80-col width). It slips through because |
||
| expect(await pending).toEqual([ | ||
| { status: "rejected", reason: new Error("Plugin 'first' failed to load.") }, | ||
| { status: "rejected", reason: new Error("Plugin 'first' failed to load.") }, | ||
| { status: "rejected", reason: new Error("Plugin 'second' failed to load.") }, | ||
| ]); | ||
| }); | ||
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.
Timed-out plugins:
loadPluginWithTimeoutreturnsfalseafter 15s but keeps loading in the background.loadPluginsthen completes and rejects its waiters here. When the plugin finishes later,markPluginLoaded→resolve(pluginId)finds nothing, so dependents never initialize even though the plugin is running.Maybe skip rejecting plugins that timed out but haven't settled, or defer their rejection until
PLUGIN_DISABLE_TIMEOUT.