Something went wrong. Try again.
Mirror of https://github.com/improsocial/improAn extensible Bluesky client for web
Something went wrong. Try again.
3.4 kB · 122 lines
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123import { Component } from "/js/components/component.js";import { effect } from "/js/signals.js";
const CONTEXT_PREFIX = "context-";
function kebabToCamel(name) { return name.replace(/-([a-z])/g, (_, char) => char.toUpperCase());}
class PluginSlot extends Component { connectedCallback() { if (this.initialized) return; this.initialized = true; if (!this.pluginService) { throw new Error("pluginService is required"); } if (!this.interactionHandlers) { throw new Error("interactionHandlers is required"); } this._pluginRoots = new Map(); this._currentRequest = null; this._subscribe(); }
_subscribe() { this._disposeEffect?.(); const slotName = this.getAttribute("name"); if (!slotName) return; this._disposeEffect = effect(() => { this.pluginService.$slots.get(slotName); this._reconcile(); }, `plugin-slot[${slotName}]`); }
disconnectedCallback() { if (!this.initialized) return; this._disposeEffect?.(); this._disposeEffect = null; this._currentRequest = null; this._pluginRoots.clear(); }
// TODO - automatic? static get observedAttributes() { return ["name", "context-uri"]; }
attributeChangedCallback(name, oldValue, newValue) { if (!this.initialized || oldValue === newValue) return; if (name === "name") { this._subscribe(); } else { this._reconcile(); } }
_getContext() { const context = {}; for (const attr of this.attributes) { if (!attr.name.startsWith(CONTEXT_PREFIX)) continue; const key = kebabToCamel(attr.name.slice(CONTEXT_PREFIX.length)); context[key] = attr.value; } return context; }
async _reconcile() { const slotName = this.getAttribute("name"); if (!slotName) return; const context = this._getContext(); const entries = this.pluginService.getSlotEntries(slotName);
const requestToken = Symbol(); this._currentRequest = requestToken;
// Drop cached roots for plugins no longer registered for this slot. const currentIds = new Set(entries.map((entry) => entry.pluginId)); for (const pluginId of [...this._pluginRoots.keys()]) { if (!currentIds.has(pluginId)) this._pluginRoots.delete(pluginId); }
if (entries.length === 0) { this.replaceChildren(); return; }
const results = await Promise.all( entries.map(async (entry) => { try { const node = await entry.invoke(context); return { entry, node }; } catch (error) { console.error( `Plugin "${entry.pluginId}" slot "${slotName}" failed:`, error, ); return { entry, node: null }; } }), );
if (this._currentRequest !== requestToken) return;
const nextChildren = []; for (const { entry, node } of results) { if (!node) continue; let state = this._pluginRoots.get(entry.pluginId); if (!state) { const renderer = this.pluginService.getRenderer(entry.pluginId); state = { root: renderer.createRoot(), }; this._pluginRoots.set(entry.pluginId, state); } const element = state.root.render(node); nextChildren.push(element); } this.replaceChildren(...nextChildren); }}
PluginSlot.register();