diff --git a/dist/network.d.ts b/dist/network.d.ts new file mode 100644 index 0000000..a846787 --- /dev/null +++ b/dist/network.d.ts @@ -0,0 +1,52 @@ +/// +import WisdomRpcQuery from "./query/wisdom.js"; +import StreamingRpcQuery from "./query/streaming.js"; +import { RpcQueryOptions, StreamHandlerFunction } from "./types.js"; +import SimpleRpcQuery from "./query/simple.js"; +export default class RpcNetwork { + constructor(dht?: any); + private _dht; + get dht(): any; + private _majorityThreshold; + get majorityThreshold(): number; + set majorityThreshold(value: number); + private _queryTimeout; + get queryTimeout(): number; + set queryTimeout(value: number); + private _relayTimeout; + get relayTimeout(): number; + set relayTimeout(value: number); + private _relays; + get relays(): string[]; + private _ready?; + get ready(): Promise; + private _bypassCache; + get bypassCache(): boolean; + set bypassCache(value: boolean); + addRelay(pubkey: string): void; + removeRelay(pubkey: string): boolean; + clearRelays(): void; + wisdomQuery( + method: string, + module: string, + data?: object | any[], + bypassCache?: boolean, + options?: RpcQueryOptions + ): WisdomRpcQuery; + streamingQuery( + relay: Buffer | string, + method: string, + module: string, + streamHandler: StreamHandlerFunction, + data?: object | any[], + options?: RpcQueryOptions + ): StreamingRpcQuery; + simpleQuery( + relay: Buffer | string, + method: string, + module: string, + data?: object | any[], + options?: RpcQueryOptions + ): SimpleRpcQuery; +} +//# sourceMappingURL=network.d.ts.map diff --git a/dist/network.d.ts.map b/dist/network.d.ts.map new file mode 100644 index 0000000..2de052d --- /dev/null +++ b/dist/network.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"network.d.ts","sourceRoot":"","sources":["../src/network.ts"],"names":[],"mappings":";AAAA,OAAO,cAAc,MAAM,mBAAmB,CAAC;AAG/C,OAAO,iBAAiB,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AACpE,OAAO,cAAc,MAAM,mBAAmB,CAAC;AAE/C,MAAM,CAAC,OAAO,OAAO,UAAU;gBACjB,GAAG,MAAY;IAI3B,OAAO,CAAC,IAAI,CAAa;IAEzB,IAAI,GAAG,QAEN;IAED,OAAO,CAAC,kBAAkB,CAAQ;IAElC,IAAI,iBAAiB,IAAI,MAAM,CAE9B;IAED,IAAI,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAElC;IAED,OAAO,CAAC,aAAa,CAAM;IAE3B,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,IAAI,YAAY,CAAC,KAAK,EAAE,MAAM,EAE7B;IAED,OAAO,CAAC,aAAa,CAAK;IAE1B,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,IAAI,YAAY,CAAC,KAAK,EAAE,MAAM,EAE7B;IAED,OAAO,CAAC,OAAO,CAAgB;IAE/B,IAAI,MAAM,IAAI,MAAM,EAAE,CAErB;IAED,OAAO,CAAC,MAAM,CAAC,CAAgB;IAE/B,IAAI,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAKzB;IAED,OAAO,CAAC,YAAY,CAAkB;IAEtC,IAAI,WAAW,IAAI,OAAO,CAEzB;IAED,IAAI,WAAW,CAAC,KAAK,EAAE,OAAO,EAE7B;IAEM,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAK9B,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAWpC,WAAW,IAAI,IAAI;IAInB,WAAW,CAChB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,IAAI,GAAE,MAAM,GAAG,GAAG,EAAO,EACzB,WAAW,GAAE,OAAe,EAC5B,OAAO,GAAE,eAAoB,GAC5B,cAAc;IAaV,cAAc,CACnB,KAAK,EAAE,MAAM,GAAG,MAAM,EACtB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,qBAAqB,EACpC,IAAI,GAAE,MAAM,GAAG,GAAG,EAAO,EACzB,OAAO,GAAE,eAAoB,GAC5B,iBAAiB;IASb,WAAW,CAChB,KAAK,EAAE,MAAM,GAAG,MAAM,EACtB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,IAAI,GAAE,MAAM,GAAG,GAAG,EAAO,EACzB,OAAO,GAAE,eAAoB,GAC5B,cAAc;CAYlB"} \ No newline at end of file diff --git a/dist/network.js b/dist/network.js new file mode 100644 index 0000000..3ed672c --- /dev/null +++ b/dist/network.js @@ -0,0 +1,86 @@ +import WisdomRpcQuery from "./query/wisdom.js"; +// @ts-ignore +import DHT from "@hyperswarm/dht"; +import StreamingRpcQuery from "./query/streaming.js"; +import SimpleRpcQuery from "./query/simple.js"; +export default class RpcNetwork { + constructor(dht = new DHT()) { + this._dht = dht; + } + _dht; + get dht() { + return this._dht; + } + _majorityThreshold = 0.75; + get majorityThreshold() { + return this._majorityThreshold; + } + set majorityThreshold(value) { + this._majorityThreshold = value; + } + _queryTimeout = 30; + get queryTimeout() { + return this._queryTimeout; + } + set queryTimeout(value) { + this._queryTimeout = value; + } + _relayTimeout = 2; + get relayTimeout() { + return this._relayTimeout; + } + set relayTimeout(value) { + this._relayTimeout = value; + } + _relays = []; + get relays() { + return this._relays; + } + _ready; + get ready() { + if (!this._ready) { + this._ready = this._dht.ready(); + } + return this._ready; + } + _bypassCache = false; + get bypassCache() { + return this._bypassCache; + } + set bypassCache(value) { + this._bypassCache = value; + } + addRelay(pubkey) { + this._relays.push(pubkey); + this._relays = [...new Set(this._relays)]; + } + removeRelay(pubkey) { + if (!this._relays.includes(pubkey)) { + return false; + } + delete this._relays[this._relays.indexOf(pubkey)]; + this._relays = Object.values(this._relays); + return true; + } + clearRelays() { + this._relays = []; + } + wisdomQuery(method, module, data = {}, bypassCache = false, options = {}) { + return new WisdomRpcQuery(this, { + method, + module, + data, + bypassCache: bypassCache || this._bypassCache, + }, options).run(); + } + streamingQuery(relay, method, module, streamHandler, data = {}, options = {}) { + return new StreamingRpcQuery(this, relay, { method, module, data }, { streamHandler, ...options }).run(); + } + simpleQuery(relay, method, module, data = {}, options = {}) { + return new SimpleRpcQuery(this, relay, { + method, + module, + data, + }, options).run(); + } +} diff --git a/dist/query/base.d.ts b/dist/query/base.d.ts new file mode 100644 index 0000000..8979931 --- /dev/null +++ b/dist/query/base.d.ts @@ -0,0 +1,34 @@ +/// +import { Buffer } from "buffer"; +import RpcNetwork from "../network.js"; +import { RpcQueryOptions } from "../types.js"; +import type { RPCRequest, RPCResponse } from "@lumeweb/relay"; +export default abstract class RpcQueryBase { + protected _network: RpcNetwork; + protected _query: RPCRequest; + protected _options: RpcQueryOptions; + protected _promise?: Promise; + protected _timeoutTimer?: any; + protected _timeout: boolean; + protected _completed: boolean; + protected _responses: { + [relay: string]: RPCResponse; + }; + protected _errors: { + [relay: string]: any; + }; + protected _promiseResolve?: (data: any) => void; + constructor( + network: RpcNetwork, + query: RPCRequest, + options?: RpcQueryOptions + ); + get result(): Promise; + private handeTimeout; + protected resolve(data?: RPCResponse, timeout?: boolean): void; + run(): this; + protected queryRelay(relay: string | Buffer): Promise; + protected abstract checkResponses(): void; + protected abstract getRelays(): string[] | Buffer[]; +} +//# sourceMappingURL=base.d.ts.map diff --git a/dist/query/base.d.ts.map b/dist/query/base.d.ts.map new file mode 100644 index 0000000..c5ed8c9 --- /dev/null +++ b/dist/query/base.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../src/query/base.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,UAAU,MAAM,eAAe,CAAC;AACvC,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE9D,MAAM,CAAC,OAAO,CAAC,QAAQ,OAAO,YAAY;IACxC,SAAS,CAAC,QAAQ,EAAE,UAAU,CAAC;IAC/B,SAAS,CAAC,MAAM,EAAE,UAAU,CAAC;IAC7B,SAAS,CAAC,QAAQ,EAAE,eAAe,CAAC;IAEpC,SAAS,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;IAClC,SAAS,CAAC,aAAa,CAAC,EAAE,GAAG,CAAC;IAC9B,SAAS,CAAC,QAAQ,EAAE,OAAO,CAAS;IACpC,SAAS,CAAC,UAAU,EAAE,OAAO,CAAS;IACtC,SAAS,CAAC,UAAU,EAAE;QAAE,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,CAAA;KAAE,CAAM;IAC5D,SAAS,CAAC,OAAO,EAAE;QAAE,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAAA;KAAE,CAAM;IACjD,SAAS,CAAC,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,IAAI,CAAC;gBAG9C,OAAO,EAAE,UAAU,EACnB,KAAK,EAAE,UAAU,EACjB,OAAO,GAAE,eAAoB;IAO/B,IAAI,MAAM,IAAI,OAAO,CAAC,WAAW,CAAC,CAEjC;IAED,OAAO,CAAC,YAAY;IAIpB,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,WAAW,EAAE,OAAO,GAAE,OAAe,GAAG,IAAI;IAc9D,GAAG,IAAI,IAAI;cA2BF,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IAmDhE,SAAS,CAAC,QAAQ,CAAC,cAAc,IAAI,IAAI;IAEzC,SAAS,CAAC,QAAQ,CAAC,SAAS,IAAI,MAAM,EAAE,GAAG,MAAM,EAAE;CACpD"} \ No newline at end of file diff --git a/dist/query/base.js b/dist/query/base.js new file mode 100644 index 0000000..b755910 --- /dev/null +++ b/dist/query/base.js @@ -0,0 +1,104 @@ +import { clearTimeout, setTimeout } from "timers"; +import { pack, unpack } from "msgpackr"; +import { Buffer } from "buffer"; +import { isPromise } from "../util.js"; +export default class RpcQueryBase { + _network; + _query; + _options; + _promise; + _timeoutTimer; + _timeout = false; + _completed = false; + _responses = {}; + _errors = {}; + _promiseResolve; + constructor(network, query, options = {}) { + this._network = network; + this._query = query; + this._options = options; + } + get result() { + return this._promise; + } + handeTimeout() { + this.resolve(undefined, true); + } + resolve(data, timeout = false) { + clearTimeout(this._timeoutTimer); + this._timeout = timeout; + this._completed = true; + if (timeout) { + data = { + error: "timeout", + }; + } + this._promiseResolve?.(data); + } + run() { + this._promise = + this._promise ?? + new Promise((resolve) => { + this._promiseResolve = resolve; + }); + this._timeoutTimer = + this._timeoutTimer ?? + setTimeout(this.handeTimeout.bind(this), (this._options.queryTimeout || this._network.queryTimeout) * 1000); + this._network.ready.then(() => { + const promises = []; + for (const relay of this.getRelays()) { + promises.push(this.queryRelay(relay)); + } + Promise.allSettled(promises).then(() => this.checkResponses()); + }); + return this; + } + async queryRelay(relay) { + let socket; + let relayKey = relay; + if (typeof relay === "string") { + relayKey = Buffer.from(relay, "hex"); + } + if (relay instanceof Buffer) { + relayKey = relay; + relay = relay.toString("hex"); + } + try { + socket = this._network.dht.connect(relayKey); + if (isPromise(socket)) { + socket = await socket; + } + } + catch (e) { + return; + } + return new Promise((resolve, reject) => { + let timer; + socket.on("data", (res) => { + relay = relay; + if (timer && timer.close) { + clearTimeout(timer); + } + socket.end(); + const response = unpack(res); + if (response && response.error) { + this._errors[relay] = response.error; + return reject(null); + } + this._responses[relay] = response; + resolve(null); + }); + socket.on("error", (error) => { + relay = relay; + this._errors[relay] = error; + reject({ error }); + }); + socket.write("rpc"); + socket.write(pack(this._query)); + timer = setTimeout(() => { + this._errors[relay] = "timeout"; + reject(null); + }, (this._options.relayTimeout || this._network.relayTimeout) * 1000); + }); + } +} diff --git a/dist/query/simple.d.ts b/dist/query/simple.d.ts new file mode 100644 index 0000000..bbe01ac --- /dev/null +++ b/dist/query/simple.d.ts @@ -0,0 +1,18 @@ +/// +import RpcQueryBase from "./base.js"; +import RpcNetwork from "../network.js"; +import type { RPCRequest } from "@lumeweb/relay"; +import { RpcQueryOptions } from "../types.js"; +import type { Buffer } from "buffer"; +export default class SimpleRpcQuery extends RpcQueryBase { + private _relay; + constructor( + network: RpcNetwork, + relay: string | Buffer, + query: RPCRequest, + options: RpcQueryOptions + ); + protected checkResponses(): void; + protected getRelays(): string[] | Buffer[]; +} +//# sourceMappingURL=simple.d.ts.map diff --git a/dist/query/simple.d.ts.map b/dist/query/simple.d.ts.map new file mode 100644 index 0000000..b589827 --- /dev/null +++ b/dist/query/simple.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"simple.d.ts","sourceRoot":"","sources":["../../src/query/simple.ts"],"names":[],"mappings":";AAAA,OAAO,YAAY,MAAM,WAAW,CAAC;AACrC,OAAO,UAAU,MAAM,eAAe,CAAC;AACvC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAErC,MAAM,CAAC,OAAO,OAAO,cAAe,SAAQ,YAAY;IACtD,OAAO,CAAC,MAAM,CAAkB;gBAE9B,OAAO,EAAE,UAAU,EACnB,KAAK,EAAE,MAAM,GAAG,MAAM,EACtB,KAAK,EAAE,UAAU,EACjB,OAAO,EAAE,eAAe;IAM1B,SAAS,CAAC,cAAc,IAAI,IAAI;IAYhC,SAAS,CAAC,SAAS,IAAI,MAAM,EAAE,GAAG,MAAM,EAAE;CAG3C"} \ No newline at end of file diff --git a/dist/query/simple.js b/dist/query/simple.js new file mode 100644 index 0000000..5ce845b --- /dev/null +++ b/dist/query/simple.js @@ -0,0 +1,21 @@ +import RpcQueryBase from "./base.js"; +export default class SimpleRpcQuery extends RpcQueryBase { + _relay; + constructor(network, relay, query, options) { + super(network, query, options); + this._relay = relay; + } + checkResponses() { + if (Object.keys(this._responses).length) { + this.resolve(Object.values(this._responses).pop()); + return; + } + if (Object.keys(this._errors).length) { + this.resolve({ error: Object.values(this._errors).pop() }); + return; + } + } + getRelays() { + return [this._relay]; + } +} diff --git a/dist/query/streaming.d.ts b/dist/query/streaming.d.ts new file mode 100644 index 0000000..a7a6aa5 --- /dev/null +++ b/dist/query/streaming.d.ts @@ -0,0 +1,17 @@ +/// +import SimpleRpcQuery from "./simple.js"; +import { Buffer } from "buffer"; +import type { RPCRequest } from "@lumeweb/relay"; +import RpcNetwork from "../network.js"; +import { StreamingRpcQueryOptions } from "../types.js"; +export default class StreamingRpcQuery extends SimpleRpcQuery { + protected _options: StreamingRpcQueryOptions; + constructor( + network: RpcNetwork, + relay: string | Buffer, + query: RPCRequest, + options: StreamingRpcQueryOptions + ); + protected queryRelay(relay: string | Buffer): Promise; +} +//# sourceMappingURL=streaming.d.ts.map diff --git a/dist/query/streaming.d.ts.map b/dist/query/streaming.d.ts.map new file mode 100644 index 0000000..451c814 --- /dev/null +++ b/dist/query/streaming.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"streaming.d.ts","sourceRoot":"","sources":["../../src/query/streaming.ts"],"names":[],"mappings":";AAAA,OAAO,cAAc,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAIhC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD,OAAO,UAAU,MAAM,eAAe,CAAC;AACvC,OAAO,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AAEvD,MAAM,CAAC,OAAO,OAAO,iBAAkB,SAAQ,cAAc;IAC3D,SAAS,CAAC,QAAQ,EAAE,wBAAwB,CAAC;gBAE3C,OAAO,EAAE,UAAU,EACnB,KAAK,EAAE,MAAM,GAAG,MAAM,EACtB,KAAK,EAAE,UAAU,EACjB,OAAO,EAAE,wBAAwB;cAKnB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;CAwDjE"} \ No newline at end of file diff --git a/dist/query/streaming.js b/dist/query/streaming.js new file mode 100644 index 0000000..aa06668 --- /dev/null +++ b/dist/query/streaming.js @@ -0,0 +1,64 @@ +import SimpleRpcQuery from "./simple.js"; +import { Buffer } from "buffer"; +import { isPromise } from "../util.js"; +import { clearTimeout, setTimeout } from "timers"; +import { pack, unpack } from "msgpackr"; +export default class StreamingRpcQuery extends SimpleRpcQuery { + _options; + constructor(network, relay, query, options) { + super(network, relay, query, options); + this._options = options; + } + async queryRelay(relay) { + let socket; + let relayKey = relay; + if (relay === "string") { + relayKey = Buffer.from(relay, "hex"); + } + if (relay instanceof Buffer) { + relayKey = relay; + relay = relay.toString("hex"); + } + try { + socket = this._network.dht.connect(relayKey); + if (isPromise(socket)) { + socket = await socket; + } + } + catch (e) { + return; + } + return new Promise((resolve, reject) => { + let timer; + socket.on("data", (res) => { + relay = relay; + if (timer && timer.close) { + clearTimeout(timer); + } + socket.end(); + const response = unpack(res); + if (response && response.error) { + this._errors[relay] = response.error; + return reject(null); + } + if (response?.data.done) { + this._responses[relay] = {}; + resolve(null); + return; + } + this._options.streamHandler(response?.data.data); + }); + socket.on("error", (error) => { + relay = relay; + this._errors[relay] = error; + reject({ error }); + }); + socket.write("rpc"); + socket.write(pack(this._query)); + timer = setTimeout(() => { + this._errors[relay] = "timeout"; + reject(null); + }, (this._options.relayTimeout || this._network.relayTimeout) * 1000); + }); + } +} diff --git a/dist/query/wisdom.d.ts b/dist/query/wisdom.d.ts new file mode 100644 index 0000000..01440c6 --- /dev/null +++ b/dist/query/wisdom.d.ts @@ -0,0 +1,9 @@ +import RpcQueryBase from "./base.js"; +export default class WisdomRpcQuery extends RpcQueryBase { + private _maxTries; + private _tries; + protected checkResponses(): void; + private retry; + protected getRelays(): string[] | []; +} +//# sourceMappingURL=wisdom.d.ts.map diff --git a/dist/query/wisdom.d.ts.map b/dist/query/wisdom.d.ts.map new file mode 100644 index 0000000..87f5d4f --- /dev/null +++ b/dist/query/wisdom.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"wisdom.d.ts","sourceRoot":"","sources":["../../src/query/wisdom.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,WAAW,CAAC;AAOrC,MAAM,CAAC,OAAO,OAAO,cAAe,SAAQ,YAAY;IACtD,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,MAAM,CAAK;IAEnB,SAAS,CAAC,cAAc,IAAI,IAAI;IAoDhC,OAAO,CAAC,KAAK;IAWb,SAAS,CAAC,SAAS,IAAI,MAAM,EAAE,GAAG,EAAE;CAGrC"} \ No newline at end of file diff --git a/dist/query/wisdom.js b/dist/query/wisdom.js new file mode 100644 index 0000000..bb22d55 --- /dev/null +++ b/dist/query/wisdom.js @@ -0,0 +1,54 @@ +import RpcQueryBase from "./base.js"; +import { flatten } from "../util.js"; +import { Buffer } from "buffer"; +import { blake2b } from "libskynet"; +import { ERR_MAX_TRIES_HIT } from "../error.js"; +export default class WisdomRpcQuery extends RpcQueryBase { + _maxTries = 3; + _tries = 0; + checkResponses() { + const responseStore = this._responses; + const responseStoreData = Object.values(responseStore); + const responseObjects = responseStoreData.reduce((output, item) => { + const itemFlattened = flatten(item?.data).sort(); + const hash = Buffer.from(blake2b(Buffer.from(JSON.stringify(itemFlattened)))).toString("hex"); + output[hash] = item?.data; + return output; + }, {}); + const responses = responseStoreData.reduce((output, item) => { + const itemFlattened = flatten(item?.data).sort(); + const hash = Buffer.from(blake2b(Buffer.from(JSON.stringify(itemFlattened)))).toString("hex"); + output[hash] = output[hash] ?? 0; + output[hash]++; + return output; + }, {}); + for (const responseHash in responses) { + if (responses[responseHash] / responseStoreData.length >= + this._network.majorityThreshold) { + let response = responseObjects[responseHash]; + // @ts-ignore + if (null === response) { + if (this._tries <= this._maxTries) { + this._tries++; + this.retry(); + return; + } + response = { error: ERR_MAX_TRIES_HIT }; + } + this.resolve(response); + break; + } + } + } + retry() { + this._responses = {}; + this._errors = {}; + if (this._completed) { + return; + } + this.run(); + } + getRelays() { + return this._network.relays; + } +}