feat(plugins): notify extensions of settings changes

This commit is contained in:
tianma-if committed 2026-09-08 19:29:50 +08:00
1 parent 87a6e64a6d
commit 72d655a109
7 files changed
+64 -6

No files matched your search

@@ -1,5 +1,9 @@
export default {
activate(context) {
const disposeSettings = context.events.on("settings.changed", ({ key }) => {
globalThis.edgeeverPluginObservedSettings ??= [];
globalThis.edgeeverPluginObservedSettings.push({ pluginId: context.pluginId, key });
});
const disposeNote = context.events.on("note.updated", ({ note }) => {
globalThis.edgeeverPluginObservedNote = note;
});
@@ -10,6 +14,7 @@ export default {
globalThis.edgeeverPluginObservedResource = resource;
});
return () => {
disposeSettings();
disposeNote();
disposeTemplate();
disposeResource();
@@ -632,10 +632,28 @@ describe("EdgeEverPluginHost", () => {
apiVersion: "1",
entry: new URL("./plugin-host-events.fixture.mjs", import.meta.url).href,
permissions: ["notes:read", "templates:read", "resources:read"],
settings: { fields: [{ key: "mode", type: "select", label: "Mode", default: "daily", options: [
{ value: "daily", label: "Daily" },
{ value: "weekly", label: "Weekly" },
] }] },
}, "https://plugins.example/events/manifest.json");
await host.setEnabled("org.edgeever.events", true);
await host.activateEnabled();
host.installManifest({
type: "plugin",
id: "org.edgeever.events-other",
name: "Other Events",
version: "1.0.0",
apiVersion: "1",
entry: new URL("./plugin-host-events.fixture.mjs", import.meta.url).href,
permissions: [],
settings: { fields: [{ key: "mode", type: "text", label: "Mode" }] },
}, "https://plugins.example/events-other/manifest.json");
await host.setEnabled("org.edgeever.events-other", true);
await host.setSettingValue("org.edgeever.events", "mode", "weekly");
await host.removeSettingValue("org.edgeever.events", "mode");
await repositoryWithEvents.updateMemo(updatedMemo, {});
await repositoryWithEvents.createTemplate({ name: "Event template" });
await repositoryWithEvents.updateResource("resource-events", new File(["scene"], "drawing.excalidraw"), "old-hash");
@@ -652,9 +670,14 @@ describe("EdgeEverPluginHost", () => {
id: "resource-events",
contentHash: "resource-event-hash",
});
expect(globalThis.edgeeverPluginObservedSettings).toEqual([
{ pluginId: "org.edgeever.events", key: "mode" },
{ pluginId: "org.edgeever.events", key: "mode" },
]);
delete globalThis.edgeeverPluginObservedNote;
delete globalThis.edgeeverPluginObservedTemplate;
delete globalThis.edgeeverPluginObservedResource;
delete globalThis.edgeeverPluginObservedSettings;
await host.dispose();
});
});
+12 -6
View File
@@ -421,7 +421,7 @@ export class EdgeEverPluginHost {
private readonly mountedPanels = new Map<string, Set<() => void>>();
private readonly embeds = new Map<string, PluginEmbedRenderer & { pluginId: string }>();
private readonly mountedEmbeds = new Map<string, Set<() => void>>();
private readonly eventListeners = new Map<keyof PluginEventMap, Set<(payload: never) => void>>();
private readonly eventListeners = new Map<keyof PluginEventMap, Set<{ pluginId: string; listener: (payload: never) => void }>>();
private extensions = readInstalledExtensions();
private activeThemeId = readStorageItem(ACTIVE_THEME_STORAGE_KEY);
private snapshot: PluginHostSnapshot = { extensions: [], commands: [], panels: [], embeds: [], recentActions: [], activeThemeId: null };
@@ -690,9 +690,11 @@ export class EdgeEverPluginHost {
const normalized = validateSettingValue(field, value);
if (field.type === "secret") {
await this.secretStorage.set(`${this.scope}:${extensionId}`, `setting:${key}`, String(normalized));
this.emit("settings.changed", { key }, extensionId);
return;
}
writeStorageItem(`${SETTINGS_STORAGE_PREFIX}:${this.scope}:${extensionId}:${key}`, JSON.stringify(normalized));
this.emit("settings.changed", { key }, extensionId);
}
async removeSettingValue(extensionId: string, key: string) {
@@ -701,9 +703,11 @@ export class EdgeEverPluginHost {
const field = requireSettingField(extension.manifest, key);
if (field.type === "secret") {
await this.secretStorage.remove(`${this.scope}:${extensionId}`, `setting:${key}`);
this.emit("settings.changed", { key }, extensionId);
return;
}
removeStorageItem(`${SETTINGS_STORAGE_PREFIX}:${this.scope}:${extensionId}:${key}`);
this.emit("settings.changed", { key }, extensionId);
}
async runCommand(pluginId: string, commandId: string) {
@@ -1205,9 +1209,10 @@ export class EdgeEverPluginHost {
const permission = EVENT_PERMISSIONS[event];
if (permission) assertPermission(manifest, permission);
const listeners = this.eventListeners.get(event) ?? new Set();
listeners.add(listener as (payload: never) => void);
const entry = { pluginId: manifest.id, listener: listener as (payload: never) => void };
listeners.add(entry);
this.eventListeners.set(event, listeners);
const dispose = () => listeners.delete(listener as (payload: never) => void);
const dispose = () => listeners.delete(entry);
disposers.push(dispose);
return dispose;
},
@@ -1456,10 +1461,11 @@ export class EdgeEverPluginHost {
}
}
private emit<K extends keyof PluginEventMap>(event: K, payload: PluginEventMap[K]) {
for (const listener of this.eventListeners.get(event) ?? []) {
private emit<K extends keyof PluginEventMap>(event: K, payload: PluginEventMap[K], targetPluginId?: string) {
for (const entry of this.eventListeners.get(event) ?? []) {
if (targetPluginId && entry.pluginId !== targetPluginId) continue;
try {
const result = (listener as (value: never) => unknown)(payload as never);
const result = (entry.listener as (value: never) => unknown)(payload as never);
if (result && typeof (result as PromiseLike<unknown>).then === "function") {
void Promise.resolve(result).catch((error) => console.error(`Plugin event listener failed for ${event}`, error));
}
+10
View File
@@ -307,6 +307,16 @@ await context.settings.set("format", "html");
await context.settings.remove("token");
```
Plugins can listen for changes to their own settings and then read the host-validated value again. The event is delivered only to the plugin that owns the setting and does not include the value, keeping secrets and other configuration out of event payloads:
```ts
context.events.on("settings.changed", async ({ key }) => {
if (key !== "format") return;
const format = await context.settings.get("format");
// Apply the updated format.
});
```
## Storage and network
Plugin storage is namespaced by EdgeEver workspace and plugin ID:
+10
View File
@@ -307,6 +307,16 @@ await context.settings.set("format", "html");
await context.settings.remove("token");
```
插件可以监听自己设置的变化,并重新读取宿主已经校验过的值。事件只会发送给拥有该设置的插件,且不会携带设置值,避免 secret 或其他配置进入事件载荷:
```ts
context.events.on("settings.changed", async ({ key }) => {
if (key !== "format") return;
const format = await context.settings.get("format");
// 应用更新后的格式。
});
```
## 插件存储与网络
插件存储按照 EdgeEver 工作区和插件 ID 隔离:
+3
View File
@@ -210,6 +210,9 @@ export interface PluginTemplate {
updatedAt: string;
}
export type PluginEventMap = {
"settings.changed": {
key: string;
};
"note.created": {
note: PluginNote;
};
+1
View File
@@ -260,6 +260,7 @@ export interface PluginTemplate {
}
export type PluginEventMap = {
"settings.changed": { key: string };
"note.created": { note: PluginNote };
"note.updated": { note: PluginNote };
"note.deleted": { noteId: string };