From 00fc999169f774331bc1becc5bdd51162a50d7f0 Mon Sep 17 00:00:00 2001 From: Derrick Hammer Date: Sat, 26 Nov 2022 02:59:07 -0500 Subject: [PATCH] *Major refactor to swarm based DHT design, switch to protomux-rpc, merging core plugin into main daemon, and creating a new internal rpc plugin for cache and broadcast requests --- nfpm.yaml | 2 - package.json | 13 +- src/modules/app.ts | 2 +- src/modules/dns.ts | 8 +- src/modules/plugin.ts | 19 +- src/modules/plugins/core.ts | 23 +++ src/modules/plugins/rpc.ts | 132 ++++++++++++++ src/modules/relay.ts | 4 +- src/modules/rpc.ts | 27 ++- src/modules/rpc/cache.ts | 135 ++++++++++++++ src/modules/rpc/server.ts | 247 ++++++++++++++++++++++++++ src/modules/{dht.ts => swarm.ts} | 38 ++-- src/rpc/connection.ts | 90 ---------- src/rpc/server.ts | 238 ------------------------- src/types.ts | 21 --- yarn.lock | 290 ++++++++++++++++++++++++++++++- 16 files changed, 896 insertions(+), 393 deletions(-) create mode 100644 src/modules/plugins/core.ts create mode 100644 src/modules/plugins/rpc.ts create mode 100644 src/modules/rpc/cache.ts create mode 100644 src/modules/rpc/server.ts rename src/modules/{dht.ts => swarm.ts} (58%) delete mode 100644 src/rpc/connection.ts delete mode 100644 src/rpc/server.ts delete mode 100644 src/types.ts diff --git a/nfpm.yaml b/nfpm.yaml index 24bd116..8dfd7a1 100644 --- a/nfpm.yaml +++ b/nfpm.yaml @@ -15,8 +15,6 @@ contents: - src: ./systemd.service dst: /etc/systemd/system/lumeweb-relay.service type: config -depends: - - lumeweb-relay-plugin-core scripts: postinstall: ./pkg/scripts/postinstall.sh preremove: ./pkg/scripts/preremove.sh diff --git a/package.json b/package.json index 602cffe..120d7fa 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@hyperswarm/dht": "^6.0.1", "@hyperswarm/dht-relay": "^0.3.0", "@lumeweb/cfg": "https://github.com/LumeWeb/bcfg.git", + "@lumeweb/dht-cache": "https://git.lumeweb.com/LumeWeb/dht-cache.git", "@lumeweb/kernel-utils": "https://github.com/LumeWeb/kernel-utils.git", "@lumeweb/pokt-rpc-endpoints": "https://github.com/LumeWeb/pokt-rpc-endpoints.git", "@skynetlabs/skynet-nodejs": "^2.6.0", @@ -31,11 +32,14 @@ "@types/ws": "^8.5.3", "ajv": "^8.11.0", "async-mutex": "^0.3.2", + "b4a": "^1.6.1", + "compact-encoding": "^2.11.0", "date-fns": "^2.28.0", "dotenv": "^16.0.1", "ethers": "^5.6.9", "express": "^4.18.1", "fetch-blob": "https://github.com/LumeWeb/fetch-blob.git", + "hyperswarm": "^3.0.4", "json-stable-stringify": "^1.0.1", "libskynet": "https://github.com/LumeWeb/libskynet.git", "libskynetnode": "https://github.com/LumeWeb/libskynetnode.git", @@ -45,13 +49,18 @@ "node-cache": "^5.1.2", "node-cron": "^3.0.1", "node-fetch": "2", + "ordered-json": "^0.1.1", "promise-retry": "^2.0.1", + "protomux": "^3.4.0", + "protomux-rpc": "^1.3.0", "random-access-memory": "^4.1.0", "random-key": "^0.3.2", - "slugify": "^1.6.5" + "slugify": "^1.6.5", + "sodium-universal": "^3.1.0" }, "devDependencies": { - "@lumeweb/relay-types": "https://github.com/LumeWeb/relay-types.git", + "@lumeweb/relay-types": "https://git.lumeweb.com/LumeWeb/relay-types.git", + "@types/b4a": "^1.6.0", "@types/express": "^4.17.13", "@types/minimatch": "^3.0.5", "@types/node-fetch": "^2.6.2", diff --git a/src/modules/app.ts b/src/modules/app.ts index aa65152..b9c09ba 100644 --- a/src/modules/app.ts +++ b/src/modules/app.ts @@ -2,7 +2,7 @@ import express, { Express } from "express"; import http from "http"; import { AddressInfo } from "net"; import log from "loglevel"; -import { getKeyPair } from "./dht"; +import { getKeyPair } from "./swarm.js"; let app: Express; let router = express.Router(); diff --git a/src/modules/dns.ts b/src/modules/dns.ts index 4c57438..f5aa561 100644 --- a/src/modules/dns.ts +++ b/src/modules/dns.ts @@ -1,5 +1,5 @@ import cron from "node-cron"; -import { get as getDHT } from "./dht.js"; +import { get as getSwarm } from "./swarm.js"; import { Buffer } from "buffer"; import { pack } from "msgpackr"; import config from "../config.js"; @@ -36,19 +36,19 @@ async function ipUpdate() { } export async function start() { - const dht = (await getDHT()) as any; + const swarm = (await getSwarm()) as any; await ipUpdate(); await overwriteRegistryEntry( - dht.defaultKeyPair, + swarm.dht.defaultKeyPair, hashDataKey(REGISTRY_NODE_KEY), pack(`${config.str("domain")}:${config.uint("port")}`) ); log.info( "Relay Identity is", - Buffer.from(dht.defaultKeyPair.publicKey).toString("hex") + Buffer.from(swarm.dht.defaultKeyPair.publicKey).toString("hex") ); cron.schedule("0 * * * *", ipUpdate); diff --git a/src/modules/plugin.ts b/src/modules/plugin.ts index a11c90f..fe3a48f 100644 --- a/src/modules/plugin.ts +++ b/src/modules/plugin.ts @@ -1,5 +1,5 @@ import config from "../config.js"; -import { getRpcServer } from "../rpc/server.js"; +import { getRpcServer } from "./rpc/server.js"; import type { PluginAPI, RPCMethod, Plugin } from "@lumeweb/relay-types"; import slugify from "slugify"; import * as fs from "fs"; @@ -22,6 +22,8 @@ import { overwriteIndependentFileSmall, } from "../lib/file"; import { setDnsProvider } from "./dns"; +import pluginRpc from "./plugins/rpc"; +import pluginCore from "./plugins/core"; let pluginApi: PluginApiManager; @@ -57,6 +59,11 @@ export class PluginApiManager { } catch (e) { throw e; } + + return this.loadPluginInstance(plugin); + } + + public async loadPluginInstance(plugin: Plugin): Promise { if ("default" in plugin) { plugin = plugin?.default as Plugin; } @@ -81,8 +88,7 @@ export class PluginApiManager { getRpcServer().registerMethod(pluginName, methodName, method); }, loadPlugin: getPluginAPI().loadPlugin.bind(getPluginAPI()), - getMethods: getRpcServer().getMethods.bind(getRpcServer()), - + getRpcServer, ssl: { setContext: setSslContext, getContext: getSslContext, @@ -120,7 +126,12 @@ export function getPluginAPI(): PluginApiManager { } export async function loadPlugins() { + const api = await getPluginAPI(); + + api.loadPluginInstance(pluginCore); + api.loadPluginInstance(pluginRpc); + for (const plugin of [...new Set(config.array("plugins", []))] as []) { - await getPluginAPI().loadPlugin(plugin); + api.loadPlugin(plugin); } } diff --git a/src/modules/plugins/core.ts b/src/modules/plugins/core.ts new file mode 100644 index 0000000..4843ef9 --- /dev/null +++ b/src/modules/plugins/core.ts @@ -0,0 +1,23 @@ +import { Plugin, PluginAPI } from "@lumeweb/relay-types"; +import { getRpcServer } from "../rpc/server"; + +const plugin: Plugin = { + name: "core", + async plugin(api: PluginAPI): Promise { + api.registerMethod("ping", { + cacheable: false, + async handler(): Promise { + return "pong"; + }, + }); + + api.registerMethod("get_methods", { + cacheable: false, + async handler(): Promise { + return api.getRpcServer().getMethods(); + }, + }); + }, +}; + +export default plugin; diff --git a/src/modules/plugins/rpc.ts b/src/modules/plugins/rpc.ts new file mode 100644 index 0000000..50e7ba6 --- /dev/null +++ b/src/modules/plugins/rpc.ts @@ -0,0 +1,132 @@ +import { getRpcServer } from "../rpc/server"; +import { + Plugin, + PluginAPI, + RPCBroadcastRequest, + RPCBroadcastResponse, + RPCClearCacheRequest, + RPCClearCacheResponse, + RPCClearCacheResponseRelayList, + RPCRequest, + RPCResponse, +} from "@lumeweb/relay-types"; +import { getRpcByPeer } from "../rpc"; + +async function broadcastRequest( + request: RPCRequest, + relays: string[] +): Promise>> { + const makeRequest = async (relay: string) => { + const rpc = await getRpcByPeer(relay); + return rpc.request(`${request.module}.${request.method}`, request.data); + }; + + let relayMap = new Map>(); + + for (const relay of relays) { + relayMap.set(relay, makeRequest(relay)); + } + + await Promise.allSettled([...relays.values()]); + return relayMap; +} + +const plugin: Plugin = { + name: "rpc", + async plugin(api: PluginAPI): Promise { + api.registerMethod("get_cached_item", { + cacheable: false, + async handler(req: string): Promise { + if (typeof req !== "string") { + throw new Error("item must be a string"); + } + + const cache = getRpcServer().cache.data; + + if (!Object.keys(cache).includes(req)) { + throw new Error("item does not exist"); + } + + return { + data: true, + ...cache[req]?.value, + signature: cache[req]?.signature, + }; + }, + }); + api.registerMethod("clear_cached_item", { + cacheable: false, + async handler(req: RPCClearCacheRequest): Promise { + if (req?.relays?.length) { + let resp = await broadcastRequest( + { + module: "rpc", + method: "clear_cached_item", + data: req.request, + }, + req?.relays + ); + let results: RPCClearCacheResponse = { + relays: {}, + data: true, + signedField: "relays", + }; + + for (const relay in resp) { + let ret: RPCClearCacheResponse; + try { + ret = await resp.get(relay); + } catch (e: any) { + (results.relays as RPCClearCacheResponseRelayList)[relay] = { + error: e.message, + }; + } + } + + return results; + } + + try { + api.getRpcServer().cache.deleteItem(req.request); + } catch (e: any) { + throw e; + } + + return { + data: true, + }; + }, + }); + api.registerMethod("broadcast_request", { + cacheable: false, + async handler(req: RPCBroadcastRequest): Promise { + if (!req?.request) { + throw new Error("request required"); + } + if (!req?.relays?.length) { + throw new Error("relays required"); + } + + let resp = await broadcastRequest(req.request, req.relays); + + const result: RPCBroadcastResponse = { + relays: {}, + data: true, + signedField: "relays", + }; + for (const relay in resp) { + let ret: RPCClearCacheResponse; + try { + ret = await resp.get(relay); + } catch (e: any) { + result.relays[relay] = { error: e.message }; + } + } + + return result; + }, + }); + }, +}; + +export default plugin; diff --git a/src/modules/relay.ts b/src/modules/relay.ts index b07fae0..4f2328c 100644 --- a/src/modules/relay.ts +++ b/src/modules/relay.ts @@ -8,7 +8,7 @@ import express, { Express } from "express"; import config from "../config.js"; import * as http from "http"; import * as https from "https"; -import { get as getDHT } from "./dht.js"; +import { get as getSwarm } from "./swarm.js"; import WS from "ws"; // @ts-ignore import log from "loglevel"; @@ -20,7 +20,7 @@ import { getSslContext } from "./ssl.js"; export async function start() { const relayPort = config.uint("port"); - const dht = await getDHT(); + const dht = await getSwarm(); const statusCodeServer = http.createServer(function (req, res) { // @ts-ignore diff --git a/src/modules/rpc.ts b/src/modules/rpc.ts index 59d3b1f..b173b50 100644 --- a/src/modules/rpc.ts +++ b/src/modules/rpc.ts @@ -5,12 +5,35 @@ import config from "../config.js"; import { errorExit } from "../lib/error.js"; // @ts-ignore import stringify from "json-stable-stringify"; -import { getRpcServer } from "../rpc/server.js"; +import { getRpcServer, RPC_PROTOCOL_SYMBOL } from "./rpc/server.js"; +import { get as getSwarm, SecretStream } from "./swarm.js"; export async function start() { if (!config.str("pocket-app-id") || !config.str("pocket-app-key")) { errorExit("Please set pocket-app-id and pocket-app-key config options."); } - getRpcServer(); + (await getSwarm()).on("connection", (stream: SecretStream) => + getRpcServer().setup(stream) + ); +} + +export async function getRpcByPeer(peer: string) { + const swarm = await getSwarm(); + + if (swarm._allConnections.has(peer)) { + return swarm._allConnections.get(peer)[RPC_PROTOCOL_SYMBOL]; + } + + return new Promise((resolve) => { + const listener = () => {}; + swarm.on("connection", (peer: any, info: any) => { + if (info.publicKey.toString("hex") !== peer) { + return; + } + swarm.removeListener("connection", listener); + + resolve(peer[RPC_PROTOCOL_SYMBOL]); + }); + }); } diff --git a/src/modules/rpc/cache.ts b/src/modules/rpc/cache.ts new file mode 100644 index 0000000..8661454 --- /dev/null +++ b/src/modules/rpc/cache.ts @@ -0,0 +1,135 @@ +import EventEmitter from "events"; +import DHTCache from "@lumeweb/dht-cache"; +import { + RPCCacheData, + RPCCacheItem, + RPCRequest, + RPCResponse, +} from "@lumeweb/relay-types"; +import { getRpcByPeer } from "../rpc"; +import b4a from "b4a"; +import { get as getSwarm } from "../swarm"; +import { RPCServer } from "./server"; +// @ts-ignore +import orderedJSON from "ordered-json"; +// @ts-ignore +import crypto from "hypercore-crypto"; + +export class RPCCache extends EventEmitter { + private dhtCache?: DHTCache; + private server: RPCServer; + + private _swarm?: any; + + get swarm(): any { + return this._swarm; + } + + private _data: RPCCacheData = {}; + + get data(): RPCCacheData { + return this._data; + } + + constructor(server: RPCServer) { + super(); + this.server = server; + this.init(); + } + + public async getNodeQuery( + node: string, + queryHash: string + ): Promise { + if (!this.dhtCache?.peerHasItem(node, queryHash)) { + return false; + } + + const rpc = await getRpcByPeer(node); + + let response; + + try { + response = rpc.request("rpc.get_cached_item", queryHash) as RPCCacheItem; + } catch (e: any) { + return false; + } + + if (!this.verifyResponse(b4a.from(node, "hex") as Buffer, response)) { + return false; + } + + return { ...response?.value }; + } + + public signResponse(item: RPCCacheItem): string { + const field = item.value.signedField || "data"; + const updated = item.value.updated; + // @ts-ignore + const data = item.value[field]; + const json = orderedJSON.stringify(data); + + return this.server.signData(`${updated}${json}`); + } + + public verifyResponse(pubkey: Buffer, item: RPCCacheItem): boolean | Buffer { + const field = item.value.signedField || "data"; + const updated = item.value.updated; + // @ts-ignore + const data = item.value[field]; + const json = orderedJSON.stringify(data); + + try { + if ( + !crypto.verify( + Buffer.from(`${updated}${json}`), + Buffer.from(item?.signature as string, "hex"), + pubkey + ) + ) { + return false; + } + } catch { + return false; + } + + return true; + } + + public addItem(query: RPCRequest, response: RPCResponse) { + const queryHash = RPCServer.hashQuery(query); + + const clonedResponse = { ...response }; + + clonedResponse.updated = Date.now(); + + const item = { + value: clonedResponse, + signature: "", + }; + + item.signature = this.signResponse(item); + + this._data[queryHash] = item; + } + + public deleteItem(queryHash: string): boolean { + const cache = this.dhtCache?.cache; + + if (!cache?.includes(queryHash)) { + throw Error("item does not exist"); + } + + this.dhtCache?.removeItem(queryHash); + delete this._data[queryHash]; + + return true; + } + + private async init() { + this.dhtCache = new DHTCache(await getSwarm(), { + protocol: "lumeweb.rpccache", + }); + this._swarm = await getSwarm(); + } +} diff --git a/src/modules/rpc/server.ts b/src/modules/rpc/server.ts new file mode 100644 index 0000000..b534bf9 --- /dev/null +++ b/src/modules/rpc/server.ts @@ -0,0 +1,247 @@ +import { + RPCCacheData, + RPCCacheItem, + RPCMethod, + RPCRequest, + RPCResponse, +} from "@lumeweb/relay-types"; +import EventEmitter from "events"; +// @ts-ignore +import ProtomuxRPC from "protomux-rpc"; +import b4a from "b4a"; +import { get as getSwarm, SecretStream } from "../swarm"; +// @ts-ignore +import c from "compact-encoding"; +import DHTCache from "@lumeweb/dht-cache"; +// @ts-ignore +import crypto from "hypercore-crypto"; +// @ts-ignore +import orderedJSON from "ordered-json"; +import { Mutex } from "async-mutex"; +import { RPCCache } from "./cache"; + +const sodium = require("sodium-universal"); +let server: RPCServer; + +const RPC_PROTOCOL_ID = b4a.from("lumeweb"); +export const RPC_PROTOCOL_SYMBOL = Symbol.for(RPC_PROTOCOL_ID.toString()); + +export function getRpcServer(): RPCServer { + if (!server) { + server = new RPCServer(); + } + + return server as RPCServer; +} + +export class RPCServer extends EventEmitter { + private _modules: Map> = new Map< + string, + Map + >(); + private pendingRequests: Map = new Map(); + + private _cache: RPCCache = new RPCCache(this); + + get cache(): RPCCache { + return this._cache; + } + + public static hashQuery(query: RPCRequest): string { + const clonedQuery: RPCRequest = { + module: query.module, + method: query.method, + data: query.data, + }; + const queryHash = Buffer.allocUnsafe(32); + sodium.crypto_generichash( + queryHash, + Buffer.from(orderedJSON.stringify(clonedQuery)) + ); + return queryHash.toString("hex"); + } + + public registerMethod( + moduleName: string, + methodName: string, + options: RPCMethod + ): void { + const module = this._modules.get(moduleName); + if (module && module.get(methodName)) { + throw new Error( + `Method ${methodName} already exists for module ${moduleName}` + ); + } + + let methodMap: Map | null = null; + + if (!module) { + methodMap = new Map(); + this._modules.set(moduleName, methodMap); + } + + if (!methodMap) { + methodMap = this._modules.get(moduleName) as Map; + } + + methodMap.set(methodName, options); + } + + public getMethods(): string[] { + const methods = []; + + for (const module of this._modules.keys()) { + for (const method of ( + this._modules.get(module) as Map + ).keys()) { + methods.push(`${module}.${method}`); + } + } + + return methods; + } + + public setup(stream: SecretStream) { + const existing = stream[RPC_PROTOCOL_SYMBOL]; + if (existing) return existing; + + const options = { + id: RPC_PROTOCOL_ID, + valueEncoding: c.json, + }; + const rpc = new ProtomuxRPC(stream, options); + + stream[RPC_PROTOCOL_SYMBOL] = rpc; + + for (const module of this._modules.keys()) { + for (const method of ( + this._modules.get(module) as Map + ).keys()) { + rpc.respond(`${module}.${method}`, {}, (data: any) => + this.handleRequest({ module, method, data }) + ); + } + } + + return rpc; + } + + public signData(data: any): string { + let raw = data; + if (typeof data !== "string") { + raw = orderedJSON.stringify(data); + } + + return crypto + .sign(Buffer.from(raw, this._cache.swarm.keyPair.privateKey)) + .toString("hex"); + } + + private async handleRequest(request: RPCRequest) { + let lockedRequest = await this.waitOnRequestLock(request); + + if (lockedRequest) { + return lockedRequest; + } + + let cachedRequest = this.getCachedRequest(request) as RPCCacheItem; + + if (cachedRequest) { + return cachedRequest.value; + } + + let method = this.getMethodByRequest(request) as RPCMethod; + + let ret; + let error; + + try { + ret = (await method.handler(request.data)) as RPCResponse | any; + } catch (e) { + error = e; + } + + if (error) { + throw error; + } + + let rpcResult: RPCResponse = {}; + + if (ret?.data) { + rpcResult = { ...ret }; + + const field = rpcResult?.signedField || "data"; + + // @ts-ignore + rpcResult.signature = this.signData(rpcResult[field]); + } else { + rpcResult = { + data: ret, + signature: this.signData(ret), + }; + } + + if (method.cacheable) { + this.cache.addItem(request, rpcResult); + } + + return rpcResult; + } + + private getCachedRequest(request: RPCRequest): RPCCacheItem | boolean { + const req = RPCServer.hashQuery(request); + if (RPCServer.hashQuery(request) in this._cache.data) { + this._cache.data[req]; + } + + return false; + } + + private getMethodByRequest(request: RPCRequest): Error | RPCMethod { + return this.getMethod(request.module, request.method); + } + + private getMethod(moduleName: string, method: string): Error | RPCMethod { + let item: any = this._modules.get(moduleName); + + if (!item) { + return new Error("INVALID_MODULE"); + } + + item = item.get(method); + + if (!item) { + return new Error("INVALID_METHOD"); + } + + return item; + } + + private async waitOnRequestLock( + request: RPCRequest + ): Promise { + let method = this.getMethodByRequest(request) as RPCMethod; + if (!method.cacheable) { + return; + } + + const reqId = RPCServer.hashQuery(request); + + let lock: Mutex = this.pendingRequests.get(reqId) as Mutex; + const lockExists = !!lock; + + if (!lockExists) { + lock = new Mutex(); + this.pendingRequests.set(reqId, lock); + } + + if (lock.isLocked()) { + await lock.waitForUnlock(); + if (reqId in this._cache.data) { + return this._cache.data[reqId] as RPCCacheItem; + } + } + + await lock.acquire(); + } +} diff --git a/src/modules/dht.ts b/src/modules/swarm.ts similarity index 58% rename from src/modules/dht.ts rename to src/modules/swarm.ts index 159cc52..6f190b8 100644 --- a/src/modules/dht.ts +++ b/src/modules/swarm.ts @@ -1,6 +1,8 @@ //const require = createRequire(import.meta.url); //import { createRequire } from "module"; +// @ts-ignore +import Hyperswarm from "hyperswarm"; // @ts-ignore import DHT from "@hyperswarm/dht"; import config from "../config.js"; @@ -11,13 +13,15 @@ import { validSeedPhrase, } from "libskynet"; -let node: { - ready: () => any; - createServer: () => any; - defaultKeyPair: any; - on: any; -}; -let server: any; +// @ts-ignore +import sodium from "sodium-universal"; +import b4a from "b4a"; + +const LUMEWEB = b4a.from("lumeweb"); + +export type SecretStream = any; + +let node: Hyperswarm; export function getKeyPair() { const seed = config.str("seed"); @@ -33,26 +37,22 @@ export function getKeyPair() { async function start() { const keyPair = getKeyPair(); - node = new DHT({ keyPair }); + node = new Hyperswarm({ keyPair, dht: new DHT({ keyPair }) }); + const topic = b4a.allocUnsafe(32); + sodium.crypto_generichash(topic, LUMEWEB); - await node.ready(); - - server = node.createServer(); - await server.listen(keyPair); + // @ts-ignore + await node.dht.ready(); + await node.listen(); + node.join(topic); return node; } -export async function get( - ret: "server" | "dht" = "dht" -): Promise { +export async function get(): Promise { if (!node) { await start(); } - if (ret == "server") { - return server; - } - return node; } diff --git a/src/rpc/connection.ts b/src/rpc/connection.ts deleted file mode 100644 index 4869625..0000000 --- a/src/rpc/connection.ts +++ /dev/null @@ -1,90 +0,0 @@ -import type { - RPCRequest, - RPCResponse, - RPCStreamHandler, - StreamFileResponse, -} from "@lumeweb/relay-types"; -import { pack, unpack } from "msgpackr"; -import log from "loglevel"; -import { getRpcServer } from "./server"; - -interface CancelRequest { - cancel: true; -} - -export default class RPCConnection { - private _socket: any; - private _canceled = false; - constructor(socket: any) { - this._socket = socket; - socket.rawStream._ondestroy = () => false; - socket.once("data", this.checkRpc.bind(this)); - } - - private async checkRpc(data: Buffer) { - if (data.toString() === "rpc") { - this._socket.once("data", this.processRequest); - this._socket.on("data", this.listenForCancel); - } - } - private async listenForCancel(data: Buffer) { - let request: any; - try { - request = unpack(data) as CancelRequest; - } catch (e) { - return; - } - - if (request.cancel) { - this._canceled = true; - } - } - private async processRequest(data: Buffer) { - let request: RPCRequest; - try { - request = unpack(data) as RPCRequest; - } catch (e) { - return; - } - - const that = this as any; - let response; - - const handleStream: RPCStreamHandler = async ( - stream: AsyncIterable - ): Promise => { - const emptyData = Uint8Array.from([]); - const streamResp = { - data: { - data: emptyData, - done: false, - } as StreamFileResponse, - }; - for await (const chunk of stream) { - if (this._canceled) { - break; - } - streamResp.data.data = chunk as unknown as Uint8Array; - await new Promise((resolve) => setTimeout(resolve, 15)); - that.write(pack(streamResp)); - } - - streamResp.data.data = emptyData; - streamResp.data.done = true; - return streamResp; - }; - - try { - response = await getRpcServer().handleRequest(request, handleStream); - } catch (error) { - log.trace(error); - that.write(pack({ error })); - that.end(); - return; - } - if (!this._canceled) { - that.write(pack(response)); - } - that.end(); - } -} diff --git a/src/rpc/server.ts b/src/rpc/server.ts deleted file mode 100644 index 8f71adc..0000000 --- a/src/rpc/server.ts +++ /dev/null @@ -1,238 +0,0 @@ -import { - RPCMethod, - RPCRequest, - RPCResponse, - RPCStreamHandler, -} from "@lumeweb/relay-types"; -import NodeCache from "node-cache"; -import { get as getDHT } from "../modules/dht.js"; -import { Mutex } from "async-mutex"; -import crypto from "crypto"; - -// @ts-ignore -import stringify from "json-stable-stringify"; -import Ajv from "ajv"; -import RPCConnection from "./connection.js"; -import { RPC_REQUEST_SCHEMA } from "../types.js"; - -const ajv = new Ajv({ allowUnionTypes: true }); -const validateRpcRequest = ajv.compile(RPC_REQUEST_SCHEMA); - -let server: RPCServer; - -export function getRpcServer(): RPCServer { - if (!server) { - server = new RPCServer(); - } - - return server as RPCServer; -} -export class RPCServer { - private methods = new Map>(); - private pendingRequests = new NodeCache(); - private processedRequests = new NodeCache({ - stdTTL: 60 * 60 * 12, - }); - - constructor() { - this.init(); - } - - public registerMethod( - moduleName: string, - methodName: string, - options: RPCMethod - ): void { - const module = this.methods.get(moduleName); - if (module && module.get(methodName)) { - throw new Error( - `Method ${methodName} already exists for module ${moduleName}` - ); - } - - let methodMap: Map | null = null; - - if (!module) { - methodMap = new Map(); - this.methods.set(moduleName, methodMap); - } - - if (!methodMap) { - methodMap = this.methods.get(moduleName) as Map; - } - - methodMap.set(methodName, options); - } - - public getMethods(): string[] { - const methods = []; - - for (const module of this.methods.keys()) { - for (const method of ( - this.methods.get(module) as Map - ).keys()) { - methods.push(`${module}.${method}`); - } - } - - return methods; - } - - private async init(): Promise { - (await getDHT("server")).on( - "connection", - (socket: any) => new RPCConnection(socket) - ); - } - - public async handleRequest( - request: RPCRequest, - streamHandler: RPCStreamHandler - ): Promise { - let valid = this.verifyRequest(request); - - if (valid instanceof Error) { - return { - error: valid.message, - }; - } - - let lockedRequest = await this.waitOnRequestLock(request); - - if (lockedRequest) { - return lockedRequest; - } - - let cachedRequest = this.getCachedRequest(request); - - if (cachedRequest) { - return cachedRequest; - } - - let method = this.getMethodByRequest(request) as RPCMethod; - - let result; - let isStream: AsyncIterable | boolean = false; - const flagIsStream = (stream: AsyncIterable) => { - isStream = stream; - }; - try { - result = await method.handler(request, flagIsStream); - } catch (e) { - return { - error: (e as Error).message, - }; - } - - if (isStream) { - result = await streamHandler(isStream); - } else { - if (result && !result.error && !("data" in result)) { - result = { data: result }; - } - } - - result = result as RPCResponse; - - cachedRequest = this.getCachedRequest(request); - - if (!cachedRequest && !isStream && method.cacheable) { - this.cacheRequest(request, result); - } - - return result; - } - - private async waitOnRequestLock(request: RPCRequest) { - let method = this.getMethodByRequest(request) as RPCMethod; - if (!method.cacheable) { - return; - } - - const reqId = RPCServer.getRequestId(request); - - let lock: Mutex = this.pendingRequests.get(reqId) as Mutex; - const lockExists = !!lock; - - if (!lockExists) { - lock = new Mutex(); - this.pendingRequests.set(reqId, lock); - } - - if (lock.isLocked()) { - await lock.waitForUnlock(); - return this.processedRequests.get(reqId) as RPCResponse; - } - - await lock.acquire(); - } - - private getCachedRequest(request: RPCRequest): RPCResponse | undefined { - let method = this.getMethodByRequest(request) as RPCMethod; - if (!method.cacheable) { - return; - } - - const reqId = RPCServer.getRequestId(request); - - if (!request.bypassCache && this.processedRequests.get(reqId)) { - return this.processedRequests.get(reqId) as RPCResponse; - } - } - - private cacheRequest(request: RPCRequest, response: RPCResponse): void { - const reqId = RPCServer.getRequestId(request); - - response.updated = Date.now(); - - this.processedRequests.set(reqId, response); - } - - private static hash(data: string): string { - return crypto.createHash("sha256").update(data).digest("hex"); - } - - private static getRequestId(request: RPCRequest) { - const clonedRequest = Object.assign({}, request) as RPCRequest; - - delete clonedRequest.bypassCache; - - return RPCServer.hash(stringify(clonedRequest)); - } - - private verifyRequest(request: RPCRequest) { - let valid: boolean | Error | RPCMethod = validateRpcRequest(request); - - if (!valid) { - return new Error(ajv.errorsText(validateRpcRequest.errors)); - } - - valid = this.getMethodByRequest(request); - - if (valid instanceof Error) { - return valid; - } - - return true; - } - - private getMethodByRequest(request: RPCRequest): Error | RPCMethod { - return this.getMethod(request.module, request.method); - } - - private getMethod(moduleName: string, method: string): Error | RPCMethod { - let item: any = this.methods.get(moduleName); - - if (!item) { - return new Error("INVALID_MODULE"); - } - - item = item.get(method); - - if (!item) { - return new Error("INVALID_METHOD"); - } - - return item; - } -} diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index 7593a6e..0000000 --- a/src/types.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { JSONSchemaType } from "ajv"; - -// @ts-ignore -export const RPC_REQUEST_SCHEMA: JSONSchemaType = { - type: "object", - properties: { - module: { - type: "string", - }, - method: { - type: "string", - }, - data: { - type: ["number", "string", "boolean", "object", "array"], - }, - bypassCache: { - type: "boolean", - nullable: true, - }, - }, -}; diff --git a/yarn.lock b/yarn.lock index 5c068af..0c55f37 100644 --- a/yarn.lock +++ b/yarn.lock @@ -436,6 +436,40 @@ udx-native "^1.1.0" xache "^1.1.0" +"@hyperswarm/dht@next": + version "5.0.25" + resolved "https://registry.yarnpkg.com/@hyperswarm/dht/-/dht-5.0.25.tgz#eb9f4c314715723ed7e7dcec2b3dbe2722fddda2" + integrity sha512-x8Fpvp96NSb3M/0Fap2rm70obpNd0fe8oJnwZxJfIvxQtItYFVCrD8URsI+0Fxt4tNINMxnE9h3MaKoaxePP2A== + dependencies: + "@hyperswarm/secret-stream" "^5.1.0" + b4a "^1.3.1" + bind-easy "^1.0.1" + bogon "^1.0.0" + compact-encoding "^2.4.1" + compact-encoding-net "^1.0.1" + debugging-stream "^2.0.0" + dht-rpc "^5.0.1" + noise-curve-ed "^1.0.2" + noise-handshake "^2.1.0" + record-cache "^1.1.1" + safety-catch "^1.0.1" + sodium-universal "^3.0.4" + utp-native "^2.5.3" + xache "^1.0.0" + +"@hyperswarm/secret-stream@^5.1.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@hyperswarm/secret-stream/-/secret-stream-5.2.0.tgz#26621646d3f696e81a33a92f61db31b20be9bfd3" + integrity sha512-GwgLlbJV0DgvdTm0hPfyM4IWcWqJXIPCgkZ/DAh5CJ0HX8WW/4pDw70h7fRK5zBU1XUT6IYO2QAOeqZS+e9Dvg== + dependencies: + b4a "^1.1.0" + noise-curve-ed "^1.0.2" + noise-handshake "^2.1.0" + sodium-secretstream "^1.0.0" + sodium-universal "^3.0.4" + streamx "^2.10.2" + timeout-refresh "^2.0.0" + "@hyperswarm/secret-stream@^6.0.0": version "6.0.0" resolved "https://registry.yarnpkg.com/@hyperswarm/secret-stream/-/secret-stream-6.0.0.tgz#67db820308cc9fed899cb8f5e9f47ae819d5a4e3" @@ -488,6 +522,30 @@ dependencies: bsert "~0.0.10" +"@lumeweb/dht-cache@https://git.lumeweb.com/LumeWeb/dht-cache.git": + version "0.1.0" + resolved "https://git.lumeweb.com/LumeWeb/dht-cache.git#323530c4bc136301b94699039452b483360e4c11" + dependencies: + "@lumeweb/dht-flood" "https://git.lumeweb.com/LumeWeb/dht-flood.git" + "@protobuf-ts/plugin" "^2.8.1" + b4a "^1.6.1" + compact-encoding "^2.11.0" + hypercore-crypto "^3.3.0" + jsnetworkx "^0.3.4" + lru "^3.1.0" + ordered-json "^0.1.1" + protocol-buffers-encodings "^1.2.0" + protomux-rpc "^1.3.0" + +"@lumeweb/dht-flood@https://git.lumeweb.com/LumeWeb/dht-flood.git": + version "0.1.0" + resolved "https://git.lumeweb.com/LumeWeb/dht-flood.git#21f7f2e67d41cbd0df2f637670d87fe9c827fc8a" + dependencies: + compact-encoding "^2.11.0" + lru "^3.1.0" + protocol-buffers-encodings "^1.2.0" + protomux-rpc "^1.3.0" + "@lumeweb/kernel-utils@https://github.com/LumeWeb/kernel-utils.git": version "0.1.0" resolved "https://github.com/LumeWeb/kernel-utils.git#47a39d618e8278c1fff21bc74d7b091c8dc57224" @@ -498,9 +556,12 @@ version "0.1.0" resolved "https://github.com/LumeWeb/pokt-rpc-endpoints.git#0223b743f913aa46e914b1dc237ae2e12571d96d" -"@lumeweb/relay-types@https://github.com/LumeWeb/relay-types.git": +"@lumeweb/relay-types@../relay-types/": version "0.1.0" - resolved "https://github.com/LumeWeb/relay-types.git#83b831b62171a28a6d24dfae2df228c3849634f1" + +"@lumeweb/relay-types@https://git.lumeweb.com/LumeWeb/relay-types.git": + version "0.1.0" + resolved "https://git.lumeweb.com/LumeWeb/relay-types.git#4c618a311dc31e036d3670a37df8f835a22b5e11" "@msgpackr-extract/msgpackr-extract-darwin-arm64@2.1.2": version "2.1.2" @@ -584,6 +645,42 @@ mkdirp "^1.0.4" rimraf "^3.0.2" +"@protobuf-ts/plugin-framework@^2.8.2": + version "2.8.2" + resolved "https://registry.yarnpkg.com/@protobuf-ts/plugin-framework/-/plugin-framework-2.8.2.tgz#8699938576f68d01a9a937aa383db8fdcef6f64d" + integrity sha512-ivcJdNVB3Iee8044f8erZGBgmB6ZfQbbKyxRgDBXRVKYxsruLr432WcT5upw9autK9OnlSVLaebi8kDneFXd2g== + dependencies: + "@protobuf-ts/runtime" "^2.8.2" + typescript "^3.9" + +"@protobuf-ts/plugin@^2.8.1": + version "2.8.2" + resolved "https://registry.yarnpkg.com/@protobuf-ts/plugin/-/plugin-2.8.2.tgz#f65f4362721e6a3bb3ce04f6d9760901e576eaa7" + integrity sha512-rTPxaeKBfUar8ubKxbVdv4XL6AcGA0OOgHNHFyrfODP7Epy80omwuvgFJex1YpeNFJxm/FZXXj5Z+nHuhYEqJg== + dependencies: + "@protobuf-ts/plugin-framework" "^2.8.2" + "@protobuf-ts/protoc" "^2.8.2" + "@protobuf-ts/runtime" "^2.8.2" + "@protobuf-ts/runtime-rpc" "^2.8.2" + typescript "^3.9" + +"@protobuf-ts/protoc@^2.8.2": + version "2.8.2" + resolved "https://registry.yarnpkg.com/@protobuf-ts/protoc/-/protoc-2.8.2.tgz#45809eda3037caff300b9061262444ae7ad891ec" + integrity sha512-1e+rOgp22ElyqRWunSc8bhatJcvRe90AGPceVn67IFYzybvfKl17vP1igHddeYkN0dzOucnOrwqn2v1jnDfE2w== + +"@protobuf-ts/runtime-rpc@^2.8.2": + version "2.8.2" + resolved "https://registry.yarnpkg.com/@protobuf-ts/runtime-rpc/-/runtime-rpc-2.8.2.tgz#8af6d5eab44e2fc92cfe9a83a5c351b5f2fcdfbe" + integrity sha512-vum/Y7AXdUTWGFu7dke/jCSB9dV3Oo3iVPcce3j7KudpzzWarDkEGvXjKv3Y8zJPj5waToyxwBNSb7eo5Vw5WA== + dependencies: + "@protobuf-ts/runtime" "^2.8.2" + +"@protobuf-ts/runtime@^2.8.2": + version "2.8.2" + resolved "https://registry.yarnpkg.com/@protobuf-ts/runtime/-/runtime-2.8.2.tgz#5d5424a6ae7fb846c3f4d0f2dd6448db65bb69d6" + integrity sha512-PVxsH81y9kEbHldxxG/8Y3z2mTXWQytRl8zNS0mTPUjkEC+8GUX6gj6LsA8EFp25fAs9V0ruh+aNWmPccEI9MA== + "@skynetlabs/skynet-nodejs@^2.6.0": version "2.8.0" resolved "https://registry.yarnpkg.com/@skynetlabs/skynet-nodejs/-/skynet-nodejs-2.8.0.tgz#46972beca4a54bfaa87502663aa89baf828d8055" @@ -649,6 +746,13 @@ dependencies: acme-client "*" +"@types/b4a@^1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@types/b4a/-/b4a-1.6.0.tgz#91a22117b6c8d79963ed2eed3328143df77c2542" + integrity sha512-rYU2r5nSUPyKyufWijxgTjsFp2kLCwydj2TmKU4StJeGPHS/Fs5KHgP89DNF0jddyeAbN5mdjNDqIrjIHca60g== + dependencies: + "@types/node" "*" + "@types/body-parser@*": version "1.19.2" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" @@ -901,6 +1005,18 @@ b4a@^1.0.1, b4a@^1.1.0, b4a@^1.1.1, b4a@^1.1.4, b4a@^1.3.0, b4a@^1.3.1, b4a@^1.5 resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.6.0.tgz#5430a9cac1af388910dd1a1c1aa9d3a0a796ed68" integrity sha512-fsTxXxj1081Yq5MOQ06gZ5+e2QcSyP2U6NofdOWyq+lrNI4IjkZ+fLVmoQ6uUCiNg1NWePMMVq93vOTdbJmErw== +b4a@^1.6.0, b4a@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.6.1.tgz#9effac93a469a868d024e16fd77162c653544cbd" + integrity sha512-AsKjNhz72yxteo/0EtQEiwkMUgk/tGmycXlbG4g3Ard2/ULtNLUykGOkeK0egmN27h0xMAhb76jYccW+XTBExA== + +babel-runtime@^5: + version "5.8.38" + resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-5.8.38.tgz#1c0b02eb63312f5f087ff20450827b425c9d4c19" + integrity sha512-KpgoA8VE/pMmNCrnEeeXqFG24TIH11Z3ZaimIhJWsin8EbfZy3WzFKUTIan10ZIDgRVvi9EkLbruJElJC9dRlg== + dependencies: + core-js "^1.0.0" + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -942,6 +1058,11 @@ bigint-buffer@^1.1.5: dependencies: bindings "^1.3.0" +bind-easy@^1.0.0, bind-easy@^1.0.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/bind-easy/-/bind-easy-1.1.2.tgz#d10f9be896e53fb84f49465be5b1ab9b089dbcff" + integrity sha512-2+VjZ87WFdOFnsH4tHnmtf0HF6D2T3ZNdU1t1FYIz2jt4N3tyqbg2J0bYbflXdBkVi3xfVc8Pm8NB062SPvVVA== + bindings@^1.3.0: version "1.5.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" @@ -949,6 +1070,13 @@ bindings@^1.3.0: dependencies: file-uri-to-path "1.0.0" +bits-to-bytes@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/bits-to-bytes/-/bits-to-bytes-1.3.0.tgz#b6b0b547ff5747b0609a42e31a0b57212f09f4e7" + integrity sha512-OJoHTpFXS9bXHBCekGTByf3MqM8CGblBDIduKQeeVVeiU9dDWywSSirXIBYGgg3d1zbVuvnMa1vD4r6PA0kOKg== + dependencies: + b4a "^1.5.0" + bl@^4.0.3: version "4.1.0" resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" @@ -1222,6 +1350,13 @@ commander@^2.20.3: resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== +compact-encoding-bitfield@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/compact-encoding-bitfield/-/compact-encoding-bitfield-1.0.0.tgz#39f923263dffb68b266b6cb9dc0bae2d42cf5c4a" + integrity sha512-3nMVKUg+PF72UHfainmCL8uKvyWfxsjqOtUY+HiMPGLPCTjnwzoKfFAMo1Ad7nwTPdjBqtGK5b3BOFTFW4EBTg== + dependencies: + compact-encoding "^2.4.1" + compact-encoding-net@^1.0.1, compact-encoding-net@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/compact-encoding-net/-/compact-encoding-net-1.2.0.tgz#19d2efd55df10f2ee793576fc4483bf6d2218743" @@ -1268,6 +1403,11 @@ cookie@0.5.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== +core-js@^1.0.0: + version "1.2.7" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" + integrity sha512-ZiPp9pZlgxpWRu0M+YWbm6+aQ84XEfH1JRXvfOc/fILWI0VKhLC2LX13X1NYq4fULzLMq7Hfh43CSo2/aIaUPA== + core-util-is@~1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" @@ -1356,6 +1496,22 @@ detect-libc@^1.0.3: resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg== +dht-rpc@^5.0.1: + version "5.0.6" + resolved "https://registry.yarnpkg.com/dht-rpc/-/dht-rpc-5.0.6.tgz#f37af2ae965d71d1034f852414673ed86286f7c4" + integrity sha512-8NxLsJ3WFoM2D/kQDu/4SOMnsjm/0NmBm4cJgUS9Idj1u/AuvY0bKpTNzUsVR25SdHfSEcbjzDPq40z3I3AVwg== + dependencies: + b4a "^1.3.1" + bind-easy "^1.0.0" + compact-encoding "^2.1.0" + compact-encoding-net "^1.0.1" + fast-fifo "^1.0.0" + kademlia-routing-table "^1.0.0" + nat-sampler "^1.0.1" + sodium-universal "^3.0.4" + streamx "^2.10.3" + time-ordered-set "^1.0.2" + dht-rpc@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/dht-rpc/-/dht-rpc-6.3.0.tgz#bd9fbdc0a558182af35eb9c4e92aad38e6a6e68a" @@ -1918,6 +2074,16 @@ hypercore-crypto@^3.3.0: compact-encoding "^2.5.1" sodium-universal "^3.0.0" +hyperswarm@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/hyperswarm/-/hyperswarm-3.0.4.tgz#0b0b763bd7dd75dcd045746bd61d9cb5c38eac11" + integrity sha512-+wNuhabxPpCRzrP98dtXV/Iwq7TnvcaKYx/yHXaMqZ7qqZOc8j/5aJcUyctT/5hSx0Uflwolit9BIx3L/F800w== + dependencies: + "@hyperswarm/dht" next + b4a "^1.3.1" + events "^3.3.0" + shuffled-priority-queue "^2.1.0" + iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -2102,6 +2268,16 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== +jsnetworkx@^0.3.4: + version "0.3.4" + resolved "https://registry.yarnpkg.com/jsnetworkx/-/jsnetworkx-0.3.4.tgz#1c0025df94208ceb5cc59e658f9a9bf5fef4f6f4" + integrity sha512-3wLBxtTWsgMUADKiEXyVr6s0BNnXBtB+A13cYToatl65OFF9UG1BTOij1Jx7AhK7Q9fbrfFCNppDuSOmTFkB1Q== + dependencies: + babel-runtime "^5" + lodash "^3.3.1" + through "^2.3.6" + tiny-sprintf "^0.3.0" + json-schema-traverse@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" @@ -2213,6 +2389,11 @@ lodash.uniqby@4.5.0: lodash._baseiteratee "~4.7.0" lodash._baseuniq "~4.6.0" +lodash@^3.3.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" + integrity sha512-9mDDwqVIma6OZX79ZlDACZl8sBm0TEnkf99zV3iMA4GzkIT/9hiqP5mY0HoT1iNLCrKc/R1HByV+yJfRWVJryQ== + lodash@^4.17.20: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" @@ -2235,6 +2416,13 @@ lru-cache@^7.7.1: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.14.0.tgz#21be64954a4680e303a09e9468f880b98a0b3c7f" integrity sha512-EIRtP1GrSJny0dqb50QXRUNBxHJhcpxHC++M5tD7RYbvLLn5KVWKsbyswSSqDuU15UFi3bgTQIY8nhDMeF6aDQ== +lru@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lru/-/lru-3.1.0.tgz#ea7fb8546d83733396a13091d76cfeb4c06837d5" + integrity sha512-5OUtoiVIGU4VXBOshidmtOsvBIvcQR6FD/RzWSvaeHyxCGB+PCUCu+52lqMfdc0h/2CLvHhZS4TwUmMQrrMbBQ== + dependencies: + inherits "^2.0.1" + make-fetch-happen@^10.0.3: version "10.2.1" resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz#f5e3835c5e9817b617f2770870d9492d28678164" @@ -2538,7 +2726,7 @@ node-gyp-build-optional-packages@5.0.3: resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.3.tgz#92a89d400352c44ad3975010368072b41ad66c17" integrity sha512-k75jcVzk5wnnc/FMxsf4udAoTEUv2jY3ycfdSd3yWu6Cnd1oee6/CfZJApyscA4FJOmdoixWwiwOyf16RzD5JA== -node-gyp-build@^4.3.0, node-gyp-build@^4.4.0: +node-gyp-build@^4.2.0, node-gyp-build@^4.3.0, node-gyp-build@^4.4.0: version "4.5.0" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.5.0.tgz#7a64eefa0b21112f89f58379da128ac177f20e40" integrity sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg== @@ -2641,6 +2829,18 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" +ordered-json@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ordered-json/-/ordered-json-0.1.1.tgz#dd5392d6ef6e085fcef2a1ea57a478d30aceef6f" + integrity sha512-qw4OYAxofa+WAZAP90eoXftAErUCjs8OII5ddDzKAZBsPMpQvWEIvuKCmUgGV22Cyd3/bT6i12KeuBBZixThDg== + dependencies: + ordered-object "^0.2.0" + +ordered-object@^0.2.0: + version "0.2.3" + resolved "https://registry.yarnpkg.com/ordered-object/-/ordered-object-0.2.3.tgz#2e187a7d29f9dfdb2894b695f11899fdaa83a60c" + integrity sha512-UKBtJiO7PsKqAAenewZ/moHQIRbcjZ4HE0J/+RyzgnpCTIn5ZLe3N2izno1kViTCXtHB4xuewjPgYLEiuS6t5A== + p-is-promise@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-3.0.0.tgz#58e78c7dfe2e163cf2a04ff869e7c1dba64a5971" @@ -2800,7 +3000,26 @@ proper-lockfile@^2.0.1: graceful-fs "^4.1.2" retry "^0.10.0" -protomux@^3.1.1: +protocol-buffers-encodings@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/protocol-buffers-encodings/-/protocol-buffers-encodings-1.2.0.tgz#39900b85dcff3172a23f15bdf3fda70daa2b38d3" + integrity sha512-daeNPuKh1NlLD1uDfbLpD+xyUTc07nEtfHwmBZmt/vH0B7VOM+JOCOpDcx9ZRpqHjAiIkGqyTDi+wfGSl17R9w== + dependencies: + b4a "^1.6.0" + signed-varint "^2.0.1" + varint "5.0.0" + +protomux-rpc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/protomux-rpc/-/protomux-rpc-1.3.0.tgz#ffc735f4b05d64b053a7568e64f2c65de948e056" + integrity sha512-G+zCpEujLsW+lQ75YXVdPfXKzxnP5F7IceVyz2B1ypWdH4NDf2n2ObkoUjdUrEMWgn+3fjXdY7ehkXLac+mNPQ== + dependencies: + bits-to-bytes "^1.0.0" + compact-encoding "^2.6.1" + compact-encoding-bitfield "^1.0.0" + protomux "^3.2.1" + +protomux@^3.1.1, protomux@^3.2.1, protomux@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/protomux/-/protomux-3.4.0.tgz#b6269fc15c0bd22964acb6e8f68a002e1da9a370" integrity sha512-jZBytPrL5o+eZeSfIA+5OhPBwfS4A3DucHU4R6KkwFVr2sPqtNoKOX/xXdGzQzHzfVOqbsFfC7oFQ5FqZ6XKWw== @@ -2922,7 +3141,7 @@ readable-stream@^2.0.0, readable-stream@^2.0.6, readable-stream@^2.1.4: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: +readable-stream@^3.0.2, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -3124,6 +3343,13 @@ sha512-wasm@^2.3.1: b4a "^1.0.1" nanoassert "^2.0.0" +shuffled-priority-queue@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/shuffled-priority-queue/-/shuffled-priority-queue-2.1.0.tgz#432bf14dd90f7c4dd1705752d81aadf454fd3af6" + integrity sha512-xhdh7fHyMsr0m/w2kDfRJuBFRS96b9l8ZPNWGaQ+PMvnUnZ/Eh+gJJ9NsHBd7P9k0399WYlCLzsy18EaMfyadA== + dependencies: + unordered-set "^2.0.1" + side-channel@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" @@ -3138,6 +3364,13 @@ signal-exit@^3.0.0, signal-exit@^3.0.7: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== +signed-varint@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/signed-varint/-/signed-varint-2.0.1.tgz#50a9989da7c98c2c61dad119bc97470ef8528129" + integrity sha512-abgDPg1106vuZZOvw7cFwdCABddfJRz5akcCcchzTbhyhYnsG31y4AlZEgp315T7W3nQq5P4xeOm186ZiPVFzw== + dependencies: + varint "~5.0.0" + simple-concat@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" @@ -3254,7 +3487,7 @@ sodium-secretstream@^1.0.0: b4a "^1.1.1" sodium-universal "^3.0.4" -sodium-universal@^3.0.0, sodium-universal@^3.0.4: +sodium-universal@^3.0.0, sodium-universal@^3.0.4, sodium-universal@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/sodium-universal/-/sodium-universal-3.1.0.tgz#f2fa0384d16b7cb99b1c8551a39cc05391a3ed41" integrity sha512-N2gxk68Kg2qZLSJ4h0NffEhp4BjgWHCHXVlDi1aG1hA3y+ZeWEmHqnpml8Hy47QzfL1xLy5nwr9LcsWAg2Ep0A== @@ -3407,7 +3640,7 @@ text-encoding-utf-8@^1.0.2: resolved "https://registry.yarnpkg.com/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz#585b62197b0ae437e3c7b5d0af27ac1021e10d13" integrity sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg== -"through@>=2.2.7 <3": +"through@>=2.2.7 <3", through@^2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== @@ -3417,11 +3650,21 @@ time-ordered-set@^1.0.2: resolved "https://registry.yarnpkg.com/time-ordered-set/-/time-ordered-set-1.0.2.tgz#3bd931fc048234147f8c2b8b1ebbebb0a3ecb96f" integrity sha512-vGO99JkxvgX+u+LtOKQEpYf31Kj3i/GNwVstfnh4dyINakMgeZCpew1e3Aj+06hEslhtHEd52g7m5IV+o1K8Mw== +timeout-refresh@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/timeout-refresh/-/timeout-refresh-1.0.3.tgz#7024a8ce0a09a57acc2ea86002048e6c0bff7375" + integrity sha512-Mz0CX4vBGM5lj8ttbIFt7o4ZMxk/9rgudJRh76EvB7xXZMur7T/cjRiH2w4Fmkq0zxf2QpM8IFvOSRn8FEu3gA== + timeout-refresh@^2.0.0, timeout-refresh@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/timeout-refresh/-/timeout-refresh-2.0.1.tgz#f8ec7cf1f9d93b2635b7d4388cb820c5f6c16f98" integrity sha512-SVqEcMZBsZF9mA78rjzCrYrUs37LMJk3ShZ851ygZYW1cMeIjs9mL57KO6Iv5mmjSQnOe/29/VAfGXo+oRCiVw== +tiny-sprintf@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/tiny-sprintf/-/tiny-sprintf-0.3.0.tgz#4272fd5c1d2f92807223fc16d728fdf30595a33e" + integrity sha512-2GsAMPBTxDYKjJVsK3Do2nLAMV7hteGNTy3CuNbJwRtBGgbzuzlmYIehmzJPaPyj0IsjChgcGutBZcpCe76flg== + to-data-view@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/to-data-view/-/to-data-view-1.1.0.tgz#08d6492b0b8deb9b29bdf1f61c23eadfa8994d00" @@ -3474,6 +3717,11 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" +typescript@^3.9: + version "3.9.10" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.10.tgz#70f3910ac7a51ed6bef79da7800690b19bf778b8" + integrity sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q== + typescript@^4.7.4: version "4.8.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.3.tgz#d59344522c4bc464a65a730ac695007fdb66dd88" @@ -3509,6 +3757,11 @@ universalify@^2.0.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== +unordered-set@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/unordered-set/-/unordered-set-2.0.1.tgz#4cd0fe27b8814bcf5d6073e5f0966ec7a50841e6" + integrity sha512-eUmNTPzdx+q/WvOHW0bgGYLWvWHNT3PTKEQLg0MAQhc0AHASHVHoP/9YytYd4RBVariqno/mEUhVZN98CmD7bg== + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -3556,11 +3809,32 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== +utp-native@^2.5.3: + version "2.5.3" + resolved "https://registry.yarnpkg.com/utp-native/-/utp-native-2.5.3.tgz#7c04c2a8c2858716555a77d10adb9819e3119b25" + integrity sha512-sWTrWYXPhhWJh+cS2baPzhaZc89zwlWCfwSthUjGhLkZztyPhcQllo+XVVCbNGi7dhyRlxkWxN4NKU6FbA9Y8w== + dependencies: + napi-macros "^2.0.0" + node-gyp-build "^4.2.0" + readable-stream "^3.0.2" + timeout-refresh "^1.0.0" + unordered-set "^2.0.1" + uuid@8.3.2, uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +varint@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/varint/-/varint-5.0.0.tgz#d826b89f7490732fabc0c0ed693ed475dcb29ebf" + integrity sha512-gC13b/bWrqQoKY2EmROCZ+AR0jitc6DnDGaQ6Ls9QpKmuSgJB1eQ7H3KETtQm7qSdMWMKCmsshyCmUwMLh3OAA== + +varint@~5.0.0: + version "5.0.2" + resolved "https://registry.yarnpkg.com/varint/-/varint-5.0.2.tgz#5b47f8a947eb668b848e034dcfa87d0ff8a7f7a4" + integrity sha512-lKxKYG6H03yCZUpAGOPOsMcGxd1RHCu1iKvEHYDPmTyq2HueGhD73ssNBqqQWfvYs04G9iUFRvmAVLW20Jw6ow== + vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" @@ -3627,7 +3901,7 @@ ws@^8.5.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.8.1.tgz#5dbad0feb7ade8ecc99b830c1d77c913d4955ff0" integrity sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA== -xache@^1.1.0: +xache@^1.0.0, xache@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/xache/-/xache-1.1.0.tgz#afc20dec9ff8b2260eea03f5ad9422dc0200c6e9" integrity sha512-RQGZDHLy/uCvnIrAvaorZH/e6Dfrtxj16iVlGjkj4KD2/G/dNXNqhk5IdSucv5nSSnDK00y8Y/2csyRdHveJ+Q==