feat(plugins): notify extensions of settings changes
This commit is contained in:
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();
|
||||
});
|
||||
});
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 隔离:
|
||||
|
||||
Vendored
+3
@@ -210,6 +210,9 @@ export interface PluginTemplate {
|
||||
updatedAt: string;
|
||||
}
|
||||
export type PluginEventMap = {
|
||||
"settings.changed": {
|
||||
key: string;
|
||||
};
|
||||
"note.created": {
|
||||
note: PluginNote;
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Reference in new issue
Block a user