feat(plugin): add shared stream bridge

This commit is contained in:
t8y2 committed 2026-09-14 13:38:00 +08:00
1 parent 19365b09bb
commit 05534fce32
3 files changed
+103

No files matched your search

+1
View File
@@ -325,6 +325,7 @@ describe("PluginHostBridge", () => {
const closed = pluginSandboxDocument("<html><head></head><body></body></html>", ["host.events"]);
expect(closed).toContain("connect-src 'none';");
expect(closed).toContain("host.stream.chunk");
});
it("sends the theme in init and pushes theme updates through env messages", () => {
@@ -342,6 +342,56 @@ function pluginSdkSource(): string {
for (const byte of bytes) binary += String.fromCharCode(byte);
return btoa(binary);
};
const stream = async (method, params = {}, options = {}) => {
const streamId = options.streamId || (globalThis.crypto?.randomUUID?.() || 'stream-' + Date.now() + '-' + (++sequence));
const closeMethod = options.closeMethod || 'filesystem/stream/close';
let removeListener;
let closeRequested = false;
let resolveOpen;
let rejectOpen;
const metadata = {};
const opened = new Promise((resolve, reject) => { resolveOpen = resolve; rejectOpen = reject; });
const readable = new ReadableStream({
start(controller) {
const onEvent = (message) => {
if (message?.method !== 'host.stream.chunk' && message?.method !== 'host.stream.end' && message?.method !== 'host.stream.error') return;
const event = message.params || {};
if (event.streamId !== streamId) return;
if (message.method === 'host.stream.chunk') {
try { controller.enqueue(decode(event.dataBase64 || '')); } catch (error) { controller.error(error); }
return;
}
removeListener?.();
removeListener = undefined;
if (message.method === 'host.stream.error') {
const error = new Error(event.message || 'Plugin stream failed');
rejectOpen(error);
controller.error(error);
} else {
Object.assign(metadata, event);
controller.close();
}
};
removeListener = () => listeners.event.delete(onEvent);
listeners.event.add(onEvent);
request('backend.invoke', { method, params: { ...(params || {}), streamId }, timeoutMs: options.timeoutMs }).then(resolveOpen, (error) => {
removeListener?.();
removeListener = undefined;
rejectOpen(error);
controller.error(error);
});
},
cancel() {
removeListener?.();
removeListener = undefined;
if (closeRequested) return undefined;
closeRequested = true;
return request('backend.invoke', { method: closeMethod, params: { streamId } }).catch(() => undefined);
},
});
Object.assign(metadata, await opened);
return { stream: readable, metadata };
};
window.dbxPlugin = Object.freeze({
ready,
get context() { return context; },
@@ -349,6 +399,7 @@ function pluginSdkSource(): string {
get theme() { return theme; },
request,
invoke: (method, params, options = {}) => request('backend.invoke', { method, params, timeoutMs: options.timeoutMs }),
stream,
notify: (method, params) => request('backend.notify', { method, params }),
sendBinary: (channel, data) => {
if (typeof data === 'string') return request('backend.sendBinary', { channel, dataBase64: data });