File size: 6,501 Bytes
1944112 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | /**
* WebExtension adapter: the storage passthrough, and the message/port router.
*
* This is the whole extension-specific half of what used to be
* `src/background/background.js`. The engine no longer knows it exists — it is
* one way to reach a `ScheduledEngine`, and a page reaches the same object by
* calling its methods.
*
* The wire format is unchanged byte-for-byte from `everything-webgpu/v1`, so
* the popup, the manager page, the e2e devtest page and any external extension
* keep working with no edits.
*/
import { asEngineError } from "../engine/errors.js";
import { OP, PORT_NAME, PORT_OP, PROTOCOL } from "./protocol.js";
/**
* `browser.storage.local` already *is* the StorageAdapter shape — `get(key)`
* and `set(obj)`. The two-method interface was chosen for that reason, so this
* is a passthrough rather than a translation layer.
*
* @returns {import("../engine/model-store.js").StorageAdapter}
*/
export function webExtensionStorage() {
return browser.storage.local;
}
/**
* Wires a ScheduledEngine onto this extension's runtime messaging.
*
* @param {import("../engine/engine.js").ScheduledEngine} engine
* @returns {() => void} detach
*/
export function attachWebExtensionTransport(engine) {
const subscribers = new Set();
const unsubscribe = engine.subscribe((state) => {
broadcast({ protocol: PROTOCOL, op: PORT_OP.ENGINE_STATE, state });
});
function broadcast(msg) {
for (const port of subscribers) {
try {
port.postMessage(msg);
} catch {
subscribers.delete(port);
}
}
}
// ------------------------------------------------------------- routing ----
async function handle(msg) {
if (!msg || msg.protocol !== PROTOCOL) {
throw new Error(`Expected protocol "${PROTOCOL}", got "${msg && msg.protocol}".`);
}
switch (msg.op) {
case OP.STATUS:
return { ok: true, state: engine.state, webgpu: engine.hasWebGPU };
case OP.LIST_MODELS:
return { ok: true, models: await engine.listModels(), state: engine.state };
case OP.LOAD:
return { ok: true, state: await engine.load(msg.modelId) };
case OP.UNLOAD:
return { ok: true, state: await engine.unload() };
case OP.CHAT:
return { ok: true, ...(await engine.complete(msg)) };
case OP.BATCH:
return { ok: true, results: await engine.batch(msg) };
case OP.CANCEL:
return { ok: true, cancelled: engine.cancel(msg.id ?? msg.session) };
case OP.CONFIGURE:
return { ok: true, ...(await engine.configure(msg)) };
default:
throw new Error(`Unknown op "${msg.op}".`);
}
}
/**
* `error` stays a plain string so every existing caller keeps working; `code`
* and `detail` are added beside it. Drop the string once consumers have
* moved — the wire protocol is versioned (`everything-webgpu/v1`) and this is
* an addition, not a break.
*/
const respond = (msg) =>
handle(msg).catch((err) => {
const e = asEngineError(err);
return { ok: false, error: e.message, code: e.code, ...(e.detail ? { detail: e.detail } : {}) };
});
/**
* The allowlist is a property of *this transport*, not of the engine: it
* answers "which other extension may send me messages", a question that has
* no meaning for a page that already holds the object.
*/
async function denyExternal(sender) {
const { allowedExternalIds } = await engine.store.getSettings();
const id = sender?.id;
if (allowedExternalIds.length === 0 || allowedExternalIds.includes(id)) return null;
return { ok: false, error: `Extension "${id}" is not on this engine's allowlist.` };
}
function attachPort(port) {
if (port.name !== PORT_NAME) return;
subscribers.add(port);
port.onDisconnect.addListener(() => subscribers.delete(port));
port.postMessage({ protocol: PROTOCOL, op: PORT_OP.ENGINE_STATE, state: engine.state });
const send = (msg) => {
try {
port.postMessage(msg);
} catch {
/* port closed mid-stream */
}
};
port.onMessage.addListener(async (msg) => {
const id = msg?.id;
try {
if (!msg || msg.protocol !== PROTOCOL) throw new Error(`Expected protocol "${PROTOCOL}".`);
switch (msg.op) {
case PORT_OP.SUBSCRIBE:
return send({ protocol: PROTOCOL, op: PORT_OP.ENGINE_STATE, state: engine.state });
case PORT_OP.CHAT_STREAM: {
const result = await engine.complete(msg, (delta) =>
send({ protocol: PROTOCOL, op: PORT_OP.CHUNK, id, delta }),
);
return send({ protocol: PROTOCOL, op: PORT_OP.DONE, id, ...result });
}
case PORT_OP.BATCH_STREAM: {
const results = await engine.batch(msg, (item) =>
send({ protocol: PROTOCOL, op: PORT_OP.ITEM, id, ...item }),
);
return send({ protocol: PROTOCOL, op: PORT_OP.DONE, id, results });
}
case PORT_OP.ABORT:
return void engine.cancel(msg.session ?? id);
default:
return send({ protocol: PROTOCOL, op: msg.op, id, ...(await respond(msg)) });
}
} catch (err) {
const e = asEngineError(err);
send({
protocol: PROTOCOL,
op: PORT_OP.ERROR,
id,
error: e.message,
code: e.code,
...(e.detail ? { detail: e.detail } : {}),
});
}
});
}
const onMessage = (msg) => respond(msg);
const onMessageExternal = async (msg, sender) => (await denyExternal(sender)) ?? respond(msg);
const onConnectExternal = async (port) => {
const denial = await denyExternal(port.sender);
if (denial) {
port.postMessage({ protocol: PROTOCOL, op: PORT_OP.ERROR, error: denial.error });
port.disconnect();
return;
}
attachPort(port);
};
browser.runtime.onMessage.addListener(onMessage);
browser.runtime.onMessageExternal.addListener(onMessageExternal);
browser.runtime.onConnect.addListener(attachPort);
browser.runtime.onConnectExternal.addListener(onConnectExternal);
return () => {
browser.runtime.onMessage.removeListener(onMessage);
browser.runtime.onMessageExternal.removeListener(onMessageExternal);
browser.runtime.onConnect.removeListener(attachPort);
browser.runtime.onConnectExternal.removeListener(onConnectExternal);
unsubscribe();
subscribers.clear();
};
}
|