Spaces:
Build error
Build error
| # Inter-Plugin Communication | |
| Three mechanisms are available. Choose based on the relationship between producer and consumer. | |
| | Mechanism | Use when | | |
| |-----------|----------| | |
| | `dispatchStateChange` | Broadcasting domain state to unknown or multiple consumers. The producer doesn't target a specific plugin. | | |
| | Extension points (`static extensionPoints`) | A host plugin needs structured contributions from many plugins. Contributions are additive and defined at install/start time. | | |
| | `getDependency()` API | The interaction targets one specific plugin, or a synchronous return value is required. | | |
| Cross-plugin UI access via the global `ui` object is never correct. A plugin that needs to affect another plugin's widget must use one of the three mechanisms above. | |
| --- | |
| ## State propagation | |
| Use `dispatchStateChange` to broadcast a value that any number of plugins may react to. The producer does not know or care which plugins consume the change. | |
| ```javascript | |
| // Producer: fires when user logs in | |
| async handleLogin(user) { | |
| await this.dispatchStateChange({ user }) | |
| } | |
| // Consumer A: reacts to the same state key | |
| async onUserChange(newUser) { | |
| this.updateUserMenu(newUser) | |
| } | |
| // Consumer B: also reacts independently | |
| async onUserChange(newUser) { | |
| if (!newUser) this.clearEditor() | |
| } | |
| ``` | |
| Plugin-specific data goes in `state.ext` to avoid key collisions: | |
| ```javascript | |
| await this.dispatchStateChange({ | |
| ext: { [this.name]: { preferences: prefs } } | |
| }) | |
| get preferences() { | |
| return this.state?.ext?.[this.name]?.preferences ?? {} | |
| } | |
| ``` | |
| **Rules:** | |
| - `onStateUpdate` handlers are **observers only** β calling `dispatchStateChange` from inside one throws an error because state propagation is locked during notification. | |
| - Call `dispatchStateChange` from event handlers, UI callbacks, or top-level async operations. | |
| - When `onStateUpdate` triggers async work (e.g. an API call) whose result must be written back to state, use `scheduleStateChange` instead: | |
| ```javascript | |
| async onXmlChange(newXml) { | |
| const permissions = await this.fetchPermissions(newXml); | |
| // scheduleStateChange defers the dispatch until propagation is fully done | |
| await this.scheduleStateChange({ editorReadOnly: !permissions.canEdit }); | |
| } | |
| ``` | |
| `scheduleStateChange` is the only legitimate way to write state as a consequence of `onStateUpdate`. It is not a general escape hatch β synchronous handlers must remain pure observers. | |
| --- | |
| ## Extension points | |
| Use extension points when a host plugin needs structured contributions from multiple plugins. The host defines the contract; contributors implement it. | |
| ### Declaring and implementing an extension point | |
| ```javascript | |
| // extension-points.js | |
| export default { | |
| toolbar: { | |
| contentItems: 'toolbar.contentItems', | |
| menuItems: 'toolbar.menuItems', | |
| } | |
| } | |
| ``` | |
| ```javascript | |
| // Contributing plugin | |
| import ep from '../extension-points.js' | |
| class MyPlugin extends Plugin { | |
| static extensionPoints = [ep.toolbar.contentItems]; | |
| /** | |
| * Extension point handler for `ep.toolbar.contentItems`. | |
| * Called by ToolbarPlugin during start() to collect this plugin's toolbar contributions. | |
| * Delegates to {@link MyPlugin#getToolbarContentItems}. | |
| * @returns {Array<{element: HTMLElement, priority: number, position: string}>} | |
| */ | |
| [ep.toolbar.contentItems](...args) { return this.getToolbarContentItems(...args) } | |
| getToolbarContentItems() { | |
| return [{ element: this.#ui, priority: 5, position: 'center' }] | |
| } | |
| } | |
| ``` | |
| The base class discovers the computed method automatically. The key is the full EP path string (`"toolbar.contentItems"`), so there are no naming conflicts between different namespaces. | |
| > **ASI hazard**: always end `static extensionPoints = [...]` with a semicolon. Without it, the parser treats the following `[ep.X.Y](...)` computed method as a subscript access on the array, causing a `SyntaxError: Unexpected token '{'`. | |
| Extension point handler methods MUST be documented with JSDoc: state the EP being handled, which host plugin invokes them and when, the `Delegates to` link, and `@param`/`@returns` tags. | |
| ### Invoking an extension point (host side) | |
| ```javascript | |
| // ToolbarPlugin.start() collects all contributions | |
| const results = await this.context.invokePluginEndpoint( | |
| ep.toolbar.contentItems, [], { result: 'values', throws: false } | |
| ) | |
| for (const items of results) { | |
| if (!Array.isArray(items)) continue | |
| for (const { element, priority = 0, position = 'center' } of items) { | |
| ui.toolbar.add(element, priority, position) | |
| } | |
| } | |
| ``` | |
| ### Auto-discovered extension points | |
| The base class auto-mounts these without any declaration: | |
| - **Lifecycle methods**: `install`, `ready`, `start`, `shutdown`, `onStateUpdate` β just define the method. | |
| - **Per-key state handlers**: `on<Key>Change` β follow the naming convention, e.g. `onXmlChange`, `onUserChange`. | |
| All other extension points require `static extensionPoints` with a corresponding computed handler method. | |
| --- | |
| ## `getDependency()` API | |
| Use when the interaction is intentionally directed at one specific plugin, or when a synchronous return value is required (state and extension points are both asynchronous/broadcast). | |
| ```javascript | |
| class DocumentActionsPlugin extends Plugin { | |
| // Private getters β resolved lazily at call time, not at construction time. | |
| // This avoids initialization-order issues and circular dependency problems. | |
| get #logger() { return this.getDependency('logger') } | |
| get #xmlEditor() { return this.getDependency('xmleditor') } | |
| get #client() { return this.getDependency('client') } | |
| async saveRevision() { | |
| this.#logger.debug('saving...') | |
| const xmlDoc = this.#xmlEditor.getXmlTree() | |
| await this.#client.saveXml(xmlDoc) | |
| } | |
| } | |
| ``` | |
| Only add a plugin to `deps` when it must be fully installed before this plugin's own `install()` runs. For plugins only needed at action time (button clicks, async operations), the private getter above is sufficient β no `deps` entry required. | |
| **Check for circular deps**: before declaring a dep, verify the full chain does not lead back to the current plugin. If plugin A depends on B and B (transitively) depends on A, declare neither as a dep β use lazy `getDependency()` calls at call time instead. | |
| --- | |
| ## Decision guide | |
| ``` | |
| Need to affect another plugin's widget? | |
| β No direct ui.otherPlugin.* access allowed. | |
| β If the other plugin owns the widget, add an API method to it and use getDependency(). | |
| β If the value is domain state (e.g. readOnly, currentUser), use dispatchStateChange(). | |
| Need contributions from many plugins? | |
| β Define an extension point. Contributors implement it passively. | |
| Need a return value from one specific plugin? | |
| β getDependency().methodName() | |
| Broadcasting a domain event? | |
| β dispatchStateChange() | |
| ``` | |