@statewalker/webrun-devtools
Version:
DevTools Extension for the StateWalker WebRun framework
154 lines (145 loc) • 4.43 kB
JavaScript
import { set } from "@statewalker/getset";
import { newRegistry } from "@statewalker/utils";
import { newHttpClient } from "@statewalker/webrun-http";
import { callPort, ioSend, listenPort } from "@statewalker/webrun-ports";
import {
METHOD_ADD_LISTENER,
METHOD_DONE,
METHOD_INIT,
METHOD_NOTIFY_LISTENER,
METHOD_REMOVE_LISTENER,
METHOD_RESET_CONNECTION,
} from "../libs/constants.js";
import { decode, encode } from "../libs/serd.js";
import { newId } from "./newId.js";
export async function initApi(
port,
{ apiKey, callTimeout = 1000 * 60 * 5, closeTimeout = 1000 * 5 },
) {
const [register, cleanup] = newRegistry();
let api = {};
let listeners = {};
register(() => {
for (const { listener, removeListener } of Object.values(listeners)) {
if (typeof removeListener === "function") {
removeListener(listener);
}
}
listeners = {};
});
let connected;
async function _initCall() {
if (!connected) {
// List of listeners to re-initialize in the background script
const listenersList = Object.entries(listeners).map(
([listenerId, { listenerMethodName }]) => {
return [listenerId, listenerMethodName];
},
);
connected = callPort(
port,
{
method: METHOD_INIT,
args: [
{
apiKey,
listeners: listenersList,
},
],
},
{ timeout: callTimeout },
);
}
return connected;
}
async function _call(method, ...args) {
await _initCall();
return await callPort(
port,
{
method,
args,
},
{ timeout: callTimeout },
);
}
const cleanupPort = listenPort(port, async ({ method, args } = {}) => {
if (method === METHOD_NOTIFY_LISTENER) {
const [listenerId, ...params] = args;
const { listener } = listeners[listenerId] || {};
if (listener) {
return await listener(...params);
}
} else if (method === METHOD_RESET_CONNECTION) {
// This method is called by the injected script when the connection
// with the background is lost.
connected = null;
await _initCall();
} else {
throw new Error(`Unknown method "${method}"}`);
}
});
register(cleanupPort);
const { methods } = await _initCall();
for (const name of methods) {
const path = name.split(".");
let method;
if (name === "http.fetch") {
method = async (request, ...args) => {
if (!(request instanceof Request)) {
request = new Request(request, ...args);
}
const channelName = `channel-${Date.now()}-${String(Math.random()).substring(2)}`;
// "Preflight" call
const _promise = _call(name, request.url, {
channelName,
});
// The real call handling - in a separate channel
const client = newHttpClient((input) =>
decode(
ioSend(port, encode(input), {
channelName,
// log: console.warn.bind(console, "CLIENT")
}),
),
);
const response = await client(request);
return response;
};
} else if (name.match(/\.on[A-Z]/)) {
async function removeListener(listener) {
const listenerId = listener.__id;
delete listener.__id;
delete listeners[listenerId];
return await _call(METHOD_REMOVE_LISTENER, listenerId, name);
}
async function addListener(listener, ...args) {
const listenerId = newId("listener-");
listener.__id = listenerId;
listeners[listenerId] = {
listener,
removeListener,
listenerMethodName: name,
};
return await _call(METHOD_ADD_LISTENER, listenerId, name, ...args);
}
method = async (listener) => {
await addListener(listener);
return register(() => removeListener(listener));
};
method.addListener = addListener;
method.removeListener = removeListener;
} else {
method = async (...args) => await _call(name, ...args);
}
api = set(api, path, method);
}
api.close = async () => {
cleanup();
// Let know to the extension that this connection and associated resources
// should be cleaned up
await _call(METHOD_DONE);
setTimeout(() => port.close(), closeTimeout);
};
return api;
}