diff --git a/deno/README.md b/deno/README.md
new file mode 100644
index 0000000..20fdd9d
--- /dev/null
+++ b/deno/README.md
@@ -0,0 +1,578 @@
+# flatend
+
+[](LICENSE)
+[](https://discord.gg/HZEbkeQ)
+[](https://pkg.go.dev/github.com/lithdew/flatend)
+[](https://www.npmjs.com/package/flatend)
+[](https://www.npmjs.com/package/flatend)
+[](https://github.com/nodejs/security-wg/blob/master/processes/responsible_disclosure_template.md)
+
+
+
+**flatend** is an experimental framework and protocol to make microservices more modular, simpler, safer, cheaper, and faster to build using [p2p networking](https://github.com/lithdew/monte).
+
+**flatend** aims to provide the benefits low-code tools try to bring to increase developer productivity, but with [zero vendor lock-in](https://news.ycombinator.com/item?id=20985429), [strong performance](https://projectricochet.com/blog/top-10-meteor-performance-problems), and [zero bias towards certain coding styles/patterns](https://news.ycombinator.com/item?id=12166666).
+
+## Features
+
+- Fully agnostic and compatible with any type of language, database, tool, library, or framework.
+- P2P-based service discovery, load balancing, routing, and PKI via [Kademlia](https://en.wikipedia.org/wiki/Kademlia).
+- Fully-encrypted, end-to-end, bidirectional streaming RPC via [Monte](https://github.com/lithdew/monte).
+- Automatic reconnect/retry upon crashes or connection loss.
+- Zero-hassle serverless: every function is a microservice.
+- Stream multiple gigabytes of data across microservices.
+
+## Gateways
+
+**flatend** additionally comes with scalable, high-performance, production-ready, easily-deployable API gateways that are bundled into a [small, single executable binary](https://github.com/lithdew/flatend/releases) to help you quickly deploy your microservices.
+
+- Written in [Go](https://golang.org/).
+- HTTP/1.1, HTTP/2 support.
+- Automatic HTTPS via [LetsEncrypt](https://letsencrypt.org/).
+- Expose/load-balance across microservices.
+- Serve static files and directories.
+- REPL for real-time management (_coming soon!_).
+- Prometheus metrics (_coming soon!_).
+- WebSocket support (_coming soon!_).
+- gRPC support (_coming soon!_).
+
+All gateways have been extensively tested on [Rackspace](https://www.rackspace.com/), [Scaleway](https://www.scaleway.com/en/), [AWS](https://aws.amazon.com/), [Google Cloud](https://cloud.google.com/), and [DigitalOcean](https://www.digitalocean.com/).
+
+## Requirements
+
+Although **flatend** at its core is a protocol, and hence agnostic to whichever programming langauge you use, there are currently only two reference implementations in NodeJS and Go.
+
+- NodeJS v12.18.1+ (Windows, Linux, Mac)
+- Go v1.14.1 (Windows, Linux Mac)
+
+The rationale for starting with NodeJS and Go is so that, for any new product/service, you may:
+
+1. Quickly prototype and deploy in NodeJS with SQLite using a 2USD/month bare-metal server.
+2. Once you start scaling up, split up your microservice and rewrite the performance-critical parts in Go.
+3. Run a red/blue deployment easily to gradually deploy your new microservices and experience zero downtime.
+
+Support is planned for the following runtimes/languages:
+
+1. [Zig v0.7+](https://ziglang.org/)
+2. [Deno v1.0+](https://deno.land/)
+3. [Python v3.8+](https://www.python.org/)
+
+Have any questions? Come chat with us on [Discord](https://discord.gg/HZEbkeQ).
+
+## Usage
+
+To get started quickly, download the API gateway binary for your platform [here](https://github.com/lithdew/flatend/releases). Otherwise, build the binary from source by following the instructions [here](#build-from-source).
+
+Create a new `config.toml`, and paste in:
+
+```toml
+addr = "127.0.0.1:9000"
+
+[[http]]
+addr = ":3000"
+
+[[http.routes]]
+path = "GET /hello"
+service = "hello_world"
+```
+
+Run:
+
+```shell
+$ ./flatend
+2020/06/18 04:07:07 Listening for Flatend nodes on '127.0.0.1:9000'.
+2020/06/18 04:07:07 Listening for HTTP requests on '[::]:3000'.
+```
+
+Now, let's build your first microservice in [Go](#go)/[NodeJS](#nodejs).
+
+### Go
+
+Add [`flatend`](https://pkg.go.dev/github.com/lithdew/flatend) to a new Go modules project.
+
+```shell
+$ go mod init github.com/lithdew/flatend-testbed
+go: creating new go.mod: module github.com/lithdew/flatend-testbed
+
+$ go get github.com/lithdew/flatend
+go: downloading github.com/lithdew/flatend vX.X.X
+go: github.com/lithdew/flatend upgrade => vX.X.X
+```
+
+Write a function that describes how to handle requests for the service `hello_world` in `main.go`.
+
+```go
+package main
+
+import "github.com/lithdew/flatend"
+
+func helloWorld(ctx *flatend.Context) {
+ ctx.WriteHeader("Content-Type", "text/plain; charset=utf-8")
+ ctx.Write([]byte("Hello world!"))
+}
+```
+
+Register the function as a handler for the service `hello_world`.
+
+```go
+func main() {
+ _ = &flatend.Node{
+ Services: map[string]flatend.Handler{
+ "hello_world": helloWorld,
+ },
+ }
+}
+```
+
+Start the node and have it connect to Flatend's API gateway.
+
+```go
+func main() {
+ node := &flatend.Node{
+ Services: map[string]flatend.Handler{
+ "hello_world": helloWorld,
+ },
+ }
+ node.Start("127.0.0.1:9000")
+
+ ch := make(chan os.Signal, 1)
+ signal.Notify(ch, os.Interrupt)
+ <-ch
+
+ node.Shutdown()
+}
+```
+
+Run it.
+
+```shell
+$ go run main.go
+2020/06/18 04:09:25 Listening for Flatend nodes on '[::]:41581'.
+2020/06/18 04:09:25 You are now connected to 127.0.0.1:9000. Services: []
+2020/06/18 04:09:25 Re-probed 127.0.0.1:9000. Services: []
+2020/06/18 04:09:25 Discovered 0 peer(s).
+```
+
+Visit [localhost:3000/hello](http://localhost:3000/hello).
+
+```shell
+$ curl http://localhost:3000/hello
+Hello world!
+```
+
+Try restart your API gateway and watch your service re-discover it.
+
+```shell
+$ go run main.go
+2020/06/18 04:11:06 Listening for Flatend nodes on '[::]:39313'.
+2020/06/18 04:11:06 You are now connected to 127.0.0.1:9000. Services: []
+2020/06/18 04:11:06 Re-probed 127.0.0.1:9000. Services: []
+2020/06/18 04:11:06 Discovered 0 peer(s).
+2020/06/18 04:11:07 127.0.0.1:9000 has disconnected from you. Services: []
+2020/06/18 04:11:07 Trying to reconnect to 127.0.0.1:9000. Sleeping for 500ms.
+2020/06/18 04:11:08 Trying to reconnect to 127.0.0.1:9000. Sleeping for 617.563636ms.
+2020/06/18 04:11:08 Trying to reconnect to 127.0.0.1:9000. Sleeping for 686.907514ms.
+2020/06/18 04:11:09 You are now connected to 127.0.0.1:9000. Services: []
+```
+
+
+
+
+
+Check out more examples [here](https://github.com/lithdew/flatend/tree/master/examples/go). I recommend checking out the [Todo List](https://github.com/lithdew/flatend/tree/master/examples/go/todo) one which stores data in [SQLite](http://sqlite.org/).
+
+### NodeJS
+
+Add [`flatend`](https://www.npmjs.com/package/flatend) to a new npm/yarn project.
+
+```shell
+$ yarn init -y
+yarn init vX.X.X
+success Saved package.json
+
+$ yarn add flatend
+yarn add vX.X.X
+info No lockfile found.
+[1/4] Resolving packages...
+[2/4] Fetching packages...
+[3/4] Linking dependencies...
+[4/4] Building fresh packages...
+
+success Saved lockfile.
+success Saved X new dependencies.
+```
+
+Write a function that describes how to handle requests for the service `hello_world` in `index.js`.
+
+```js
+const { Node, Context } = require("flatend");
+
+const helloWorld = (ctx) => ctx.send("Hello world!");
+```
+
+Register the function as a handler for the service `hello_world`. Start the node and have it connect to Flatend's API gateway.
+
+```js
+const { Node, Context } = require("flatend");
+
+const helloWorld = (ctx) => ctx.send("Hello world!");
+
+async function main() {
+ await Node.start({
+ addrs: ["127.0.0.1:9000"],
+ services: {
+ hello_world: helloWorld,
+ },
+ });
+}
+
+main().catch((err) => console.error(err));
+```
+
+Run it.
+
+```shell
+$ DEBUG=* node index.js
+ flatend You are now connected to 127.0.0.1:9000. Services: [] +0ms
+ flatend Discovered 0 peer(s). +19ms
+```
+
+Visit [localhost:3000/hello](http://localhost:3000/hello).
+
+```shell
+$ curl http://localhost:3000/hello
+Hello world!
+```
+
+Try restart your API gateway and watch your service re-discover it.
+
+```shell
+$ DEBUG=* node index.js
+ flatend You are now connected to 127.0.0.1:9000. Services: [] +0ms
+ flatend Discovered 0 peer(s). +19ms
+ flatend Trying to reconnect to 127.0.0.1:9000. Sleeping for 500ms. +41s
+ flatend Trying to reconnect to 127.0.0.1:9000. Sleeping for 500ms. +504ms
+ flatend Trying to reconnect to 127.0.0.1:9000. Sleeping for 500ms. +503ms
+ flatend Trying to reconnect to 127.0.0.1:9000. Sleeping for 500ms. +503ms
+ flatend Trying to reconnect to 127.0.0.1:9000. Sleeping for 500ms. +503ms
+ flatend You are now connected to 127.0.0.1:9000. Services: [] +21ms
+```
+
+
+
+
+
+Check out more examples [here](https://github.com/lithdew/flatend/tree/master/examples/nodejs). I recommend checking out the [Todo List](https://github.com/lithdew/flatend/tree/master/examples/nodejs/todo) one which stores data in [SQLite](http://sqlite.org/).
+
+## Options
+
+### Go SDK
+
+```go
+package flatend
+
+import "github.com/lithdew/kademlia"
+
+type Node struct {
+ // A reachable, public address which peers may reach you on.
+ // The format of the address must be [host]:[port].
+ PublicAddr string
+
+ // A 32-byte Ed25519 private key. A secret key must be provided
+ // to allow for peers to reach you. A secret key may be generated
+ // by calling `flatend.GenerateSecretKey()`.
+ SecretKey kademlia.PrivateKey
+
+ // A list of addresses and ports assembled using:
+ // 1. flatend.BindAny() (bind to all hosts and any available port)
+ // 2. flatend.BindTCP(string) (binds to a [host]:[port])
+ // 3. flatend.BindTCPv4(string) (binds to an [IPv4 host]:[port])
+ // 4. flatend.BindTCPv6(string) (binds to an [IPv6 host]:[port])
+ // which your Flatend node will listen for other nodes from.
+ BindAddrs []BindFunc
+
+ // A mapping of service names to their respective handlers.
+ Services map[string]Handler
+
+ // ....
+}
+
+// Start takes in 'addrs', which is list of addresses to nodes to
+// initially reach out for/bootstrap from first.
+(*Node).Start(addrs string)
+
+import "io"
+import "io/ioutil"
+
+func helloWorld(ctx *flatend.Context) {
+ // The ID of the requester may be accessed via `ctx.ID`.
+ _ = ctx.ID
+
+ // All headers must be written before writing any response body data.
+
+ // Headers are used to send small amounts of metadata to a requester.
+
+ // For example, the HTTP API gateway directly sets headers provided
+ // as a response as the headers of a HTTP response to a HTTP request
+ // which has been transcribed to a Flatend service request that is
+ // handled by some given node.
+
+ ctx.WriteHeader("header key", "header val")
+
+ // The first response body write call will send all set headers to the
+ // requester. Any other headers set after the first call are ignored.
+ ctx.Write([]byte("Hello world!"))
+
+
+ // All request headers may be accessed via `ctx.Headers`. Headers
+ // are represented as map[string]string.
+ header, exists := ctx.Headers["params.id"]
+ _, _ = header, exists
+
+ // The body of a request may be accessed via `ctx.Body`. Request bodies
+ // are unbounded in size, and represented as a `io.ReadCloser`.
+
+ // It is advised to wrap the body under an `io.LimitReader` to limit
+ // the size of the bodies of requests.
+
+ buf, err := ioutil.ReadAll(io.LimitReader(ctx.Body, 65536))
+ _, _ = buf, err
+
+ // If no 'ctx.Write' calls are made by the end of the handler, an
+ // empty response body is provided.
+}
+```
+
+### NodeJS SDK
+
+```js
+const { Node } = require("flatend");
+
+export interface NodeOptions {
+ // A reachable, public address which peers may reach you on.
+ // The format of the address must be [host]:[port].
+ publicAddr?: string;
+
+ // A list of [host]:[port] addresses which this node will bind a listener
+ // against to accept new Flatend nodes.
+ bindAddrs?: string[];
+
+ // A list of addresses to nodes to initially reach out
+ // for/bootstrap from first.
+ addrs?: string[];
+
+ // An Ed25519 secret key. A secret key must be provided to allow for
+ // peers to reach you. A secret key may be generated by calling
+ // 'flatend.generateSecretKey()'.
+ secretKey?: Uint8Array;
+
+ // A mapping of service names to their respective handlers.
+ services?: { [key: string]: Handler };
+}
+
+await Node.start((opts: NodeOpts));
+
+const { Context } = require("flatend");
+
+// Handlers may optionally be declared as async, and may optionally
+// return promises.
+
+const helloWorld = async (ctx) => {
+ // 'ctx' is a NodeJS Duplex stream. Writing to it writes a response
+ // body, and reading from it reads a request body.
+
+ _ = ctx.id; // The ID of the requester.
+
+ ctx.pipe(ctx); // This would pipe all request data as response data.
+
+ // Headers are used to send small amounts of metadata to a requester.
+
+ // For example, the HTTP API gateway directly sets headers provided
+ // as a response as the headers of a HTTP response to a HTTP request
+ // which has been transcribed to a Flatend service request that is
+ // handled by some given node.
+
+ ctx.header("header key", "header val");
+
+ // All request headers may be accessed via 'ctx.headers'. Headers
+ // are represented as an object.
+
+ // The line below closes the response with the body being a
+ // JSON-encoded version of the request headers provided.
+
+ ctx.json(ctx.headers);
+
+ // Arbitrary streams may be piped into 'ctx', like the contents of
+ // a file for example.
+
+ const fs = require("fs");
+ fs.createFileStream("index.js").pipe(ctx);
+
+ // Any errors thrown in a handler are caught and sent as a JSON
+ // response.
+
+ throw new Error("This shouldn't happen!");
+
+ // The 'ctx' stream must be closed, either manually via 'ctx.end()' or
+ // via a function. Not closing 'ctx' will cause the handler to deadlock.
+
+ // DO NOT DO THIS!
+ // ctx.write("hello world!");
+
+ // DO THIS!
+ ctx.write("hello world!");
+ ctx.end();
+
+ // OR THIS!
+ ctx.send("hello world!");
+
+ // The line below reads the request body into a buffer up to 65536 bytes.
+ // If the body exceeds 65536 bytes, an error will be thrown.
+
+ const body = await ctx.read({ limit: 65536 });
+ console.log("I got this message:", body.toString("utf8"));
+};
+```
+
+### API Gateway
+
+The configuration file for the API gateway is written in [TOML](https://github.com/toml-lang/toml).
+
+```toml
+# Address to listen for other Flatend nodes on.
+addr = "127.0.0.1:9000"
+
+[[http]]
+https = true # Enable/disable HTTPS support. Default is false.
+
+# Domain(s) for HTTPS support. Ignored if https = false.
+domain = "lithdew.net"
+domains = ["a.lithdew.net", "b.lithdew.net"]
+
+# Addresses to serve HTTP requests on.
+# Default is :80 if https = false, and :443 if https = true.
+
+addr = ":3000"
+addrs = [":3000", ":4000", "127.0.0.1:9000"]
+
+# Remove trailing slashes in HTTP route path? Default is true.
+redirect_trailing_slash = true
+
+# Redirect to the exact configured HTTP route path? Default is true.
+redirect_fixed_path = true
+
+[http.timeout]
+read = "10s" # HTTP request read timeout. Default is 10s.
+read_header = "10s" # HTTP request header read timeout. Default is 10s.
+idle = "10s" # Idle connection timeout. Default is 10s.
+write = "10s" # HTTP response write timeout. Default is 10s.
+shutdown = "10s" # Graceful shutdown timeout. Default is 10s.
+
+[http.min]
+body_size = 1048576 # Min HTTP request body size in bytes.
+
+[http.max]
+header_size = 1048576 # Max HTTP request header size in bytes.
+body_size = 1048576 # Max HTTP request body size in bytes.
+
+# The route below serves the contents of the file 'config.toml' upon
+# recipient of a 'GET' request at path '/'. The contents of the file
+# are instructed to not be cached to the requester.
+
+# By default, caching for static files that are served is enabled.
+# Instead of a file, a directory may be statically served as well.
+
+[[http.routes]]
+path = "GET /"
+static = "config.toml"
+nocache = true
+
+# The route below takes an URL route parameter ':id', and includes it
+# in a request sent to any Flatend node we know that advertises
+# themselves of handling the service 'a', 'b', or 'c'. The HTTP
+# request body, query parameters, and headers are additionally
+# sent to the node.
+
+[[http.routes]]
+path = "POST /:id"
+services = ["a", "b", "c"]
+```
+
+## Build from source
+
+```shell
+$ git clone https://github.com/lithdew/flatend.git && cd flatend
+Cloning into 'flatend'...
+remote: Enumerating objects: 290, done.
+remote: Counting objects: 100% (290/290), done.
+remote: Compressing objects: 100% (186/186), done.
+remote: Total 1063 (delta 144), reused 231 (delta 97), pack-reused 773
+Receiving objects: 100% (1063/1063), 419.83 KiB | 796.00 KiB/s, done.
+Resolving deltas: 100% (571/571), done.
+
+$ go version
+go version go1.14.4 linux/amd64
+
+$ go build ./cmd/flatend
+```
+
+## Showcase
+
+
+
+[**Mask Demand Calculator**](https://wars-mask.surge.sh/en) - Helps you quickly calculate the amount of masks your household needs. Serving scraped RSS feeds with Flatend to more than 200K+ site visitors.
+
+## Help
+
+Got a question? Either:
+
+1. Create an [issue](https://github.com/lithdew/flatend/issues/new).
+2. Chat with us on [Discord](https://discord.gg/HZEbkeQ).
+
+## FAQ
+
+#### Is flatend production-ready? Who uses flatend today?
+
+_flatend is still a heavy work-in-progress_. That being said, it is being field tested with a few enterprise projects related to energy and IoT right now.
+
+Deployments of flatend have also been made with a few hundred thousand visitors.
+
+#### Will I be able to run flatend myself?
+
+It was built from the start to allow for self-hosting on the cloud, on bare-metal servers, in Docker containers, on Kubernetes, etc. The cloud is your limit (see the pun I did there?).
+
+#### I'm worried about vendor lock-in - what happens if flatend goes out of business?
+
+flatend's code is completely open in this single Github repository: there's no funny business going on here.
+
+The mission of flatend is to eliminate vendor lock-in and be agnostic to any kinds of hosting environments starting from day one. Also to be somewhat of a breath of fresh air to the existing low-code tools out there.
+
+#### How does flatend compare to `XXX`?
+
+flatend gives me enough flexibility as a developer to use the tools and deployment patterns I want, gives me the scalability/performance I need, and at the same time lets me be very productive in building products/services quick.
+
+flatend amalgamates a lot of what I sort of wish I had while building roughly tens of hackathon projects and startup projects.
+
+For example, in many cases I just want to spend two bucks a month knowing that the things I build can easily handle a load of thousands of request per second.
+
+Using the API gateways pre-provided with flatend, I can easily build a system that supports that and rapidly prototype its business logic in NodeJS.
+
+#### Who owns the code that I write in flatend, and the data that I and my users save in flatend?
+
+You own the data and the code. All the code is MIT licensed, and strongly compliant with GDPR/CCPA as well.
+
+All communication across microservices are fully-encrypted end-to-end using AES-256 Galois Counter Mode (GCM). Encryption keys are ephemeral and established per-session, and are established using a X25519 Diffie-Hellman handshake followed by a single pass of BLAKE-2b 256-bit.
+
+Y'know, basically just a hyper-specific standard configuration setting of the [Noise Protocol](http://www.noiseprotocol.org/).
+
+#### I have a 3rd party/legacy system that I need to use with my backend. Can I still use flatend?
+
+flatend from the start was made to be agnostic to whichever databases, programming languages, tools, or hosting environments you choose to put it through.
+
+At the end of the day, flatend is just a protocol. That being said, to use flatend with your system would require writing a sort of shim or SDK for it.
+
+Reach out to us on Discord, maybe the system you are looking to support may be an integration point well worth providing a reference implementation for.
+
+## License
+
+**flatend**, and all of its source code is released under the [MIT License](LICENSE).
diff --git a/deno/package.json b/deno/package.json
new file mode 100644
index 0000000..fde9f11
--- /dev/null
+++ b/deno/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "flatend",
+ "description": "Production-ready microservice mesh networks with just a few lines of code.",
+ "author": "Kenta Iwasaki",
+ "license": "MIT",
+ "version": "0.0.8",
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "scripts": {
+ "prepare": "yarn tsc",
+ "test": "TS_NODE_PROJECT=tests/tsconfig.json mocha -r ts-node/register tests/**/*.spec.ts"
+ },
+ "dependencies": {
+ "blake2b": "^2.1.3",
+ "debug": "^4.1.1",
+ "ipaddr.js": "^1.9.1",
+ "object-hash": "^2.0.3",
+ "tweetnacl": "^1.0.3"
+ },
+ "devDependencies": {
+ "@types/chai": "^4.2.11",
+ "@types/debug": "^4.1.5",
+ "@types/ip": "^1.1.0",
+ "@types/mocha": "^7.0.2",
+ "@types/node": "^14.0.13",
+ "@types/object-hash": "^1.3.3",
+ "chai": "^4.2.0",
+ "chai-bytes": "^0.1.2",
+ "core-js": "^3.6.5",
+ "mocha": "^8.0.1",
+ "prettier": "^2.0.5",
+ "ts-node": "^8.10.2",
+ "typescript": "^3.9.5"
+ }
+}
diff --git a/deno/src/context.ts b/deno/src/context.ts
new file mode 100644
index 0000000..8a6481b
--- /dev/null
+++ b/deno/src/context.ts
@@ -0,0 +1,141 @@
+import { Buffer } from "https://deno.land/std/node/buffer.ts";
+import { Duplex, finished } from "./std-node-stream.ts";
+import { Stream, STREAM_CHUNK_SIZE } from "./stream.ts";
+import { ID } from "./kademlia.ts";
+import * as util from 'https://deno.land/std/node/util.ts'
+import { DataPacket, Opcode, ServiceResponsePacket } from "./packet.ts";
+import { Provider } from "./provider.ts";
+import { chunkBuffer } from "./node.ts";
+
+export type BufferEncoding = string
+
+export type Handler = (ctx: Context) => void;
+
+export class Context extends Duplex {
+ _provider: Provider;
+ _stream: Stream;
+ _headersWritten = false;
+ _headers: { [key: string]: string } = {};
+
+ headers: { [key: string]: string };
+
+ get id(): ID {
+ return this._provider.id!;
+ }
+
+ constructor(
+ provider: Provider,
+ stream: Stream,
+ headers: { [key: string]: string }
+ ) {
+ super();
+
+ this._provider = provider;
+ this._stream = stream;
+ this.headers = headers;
+
+ // pipe stream body to context
+
+ setTimeout(async () => {
+ for await (const frame of this._stream.body) {
+ this.push(frame);
+ }
+ this.push(null);
+ });
+
+ // write stream eof when stream writable is closed
+
+ setTimeout(async () => {
+ await util.promisify(finished)(this, { readable: false });
+
+ await this._writeHeader();
+
+ const payload = new DataPacket(this._stream.id, Buffer.from([])).encode();
+ await this._provider.write(
+ this._provider.rpc.message(
+ 0,
+ Buffer.concat([Buffer.of(Opcode.Data), payload])
+ )
+ );
+ });
+ }
+
+ header(key: string, val: string): Context {
+ this._headers[key] = val;
+ return this;
+ }
+
+ send(data: string | Buffer | Uint8Array) {
+ this.write(data);
+ if (!this.writableEnded) this.end();
+ }
+
+ json(data: any) {
+ this.header("content-type", "application/json");
+ this.send(JSON.stringify(data));
+ }
+
+ _read(size: number) {
+ this._stream.body._read(size);
+ }
+
+ async body(opts?: { limit?: number }): Promise {
+ const limit = opts?.limit ?? 2 ** 16;
+
+ let buf = Buffer.from([]);
+ for await (const chunk of this) {
+ buf = Buffer.concat([buf, chunk]);
+ if (buf.byteLength > limit) {
+ throw new Error(
+ `Exceeded max allowed body size limit of ${limit} byte(s).`
+ );
+ }
+ }
+
+ return buf;
+ }
+
+ async _writeHeader() {
+ if (!this._headersWritten) {
+ this._headersWritten = true;
+
+ const payload = new ServiceResponsePacket(
+ this._stream.id,
+ true,
+ this._headers
+ ).encode();
+ await this._provider.write(
+ this._provider.rpc.message(
+ 0,
+ Buffer.concat([Buffer.of(Opcode.ServiceResponse), payload])
+ )
+ );
+ }
+ }
+
+ _write(
+ chunk: any,
+ encoding: BufferEncoding,
+ callback: (error?: Error | null) => void
+ ) {
+ const write = async () => {
+ await this._writeHeader();
+
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding);
+
+ for (const chunk of chunkBuffer(buf, STREAM_CHUNK_SIZE)) {
+ const payload = new DataPacket(this._stream.id, chunk).encode();
+ await this._provider.write(
+ this._provider.rpc.message(
+ 0,
+ Buffer.concat([Buffer.of(Opcode.Data), payload])
+ )
+ );
+ }
+ };
+
+ write()
+ .then(() => callback())
+ .catch((error) => callback(error));
+ }
+}
diff --git a/deno/src/kademlia.ts b/deno/src/kademlia.ts
new file mode 100644
index 0000000..6cd83bf
--- /dev/null
+++ b/deno/src/kademlia.ts
@@ -0,0 +1,192 @@
+import { Buffer } from "https://deno.land/std/node/buffer.ts";
+import * as nacl from "https://deno.land/x/tweetnacl_deno/src/nacl.ts";
+import ipaddr from "https://jspm.dev/ipaddr.js";
+import { assert } from "https://deno.land/std/testing/asserts.ts";
+
+function BufferCompare(a: Buffer | Uint8Array, b: Buffer|Uint8Array) {
+ //if (typeof a.compare === 'function') return a.compare(b)
+ if (a === b) return 0
+
+ var x = a.length
+ var y = b.length
+
+ var i = 0
+ var len = Math.min(x, y)
+ while (i < len) {
+ if (a[i] !== b[i]) break
+
+ ++i
+ }
+
+ if (i !== len) {
+ x = a[i]
+ y = b[i]
+ }
+
+ if (x < y) return -1
+ if (y < x) return 1
+ return 0
+}
+
+type IPv4 = ipaddr.IPv4;
+type IPv6 = ipaddr.IPv6;
+
+export enum UpdateResult {
+ New,
+ Ok,
+ Full,
+ Fail,
+}
+
+const leadingZeros = (buf: Uint8Array): number => {
+ const i = buf.findIndex((b) => b != 0);
+ if (i === -1) return buf.byteLength * 8;
+
+ let b = buf[i] >>> 0;
+ if (b === 0) return i * 8 + 8;
+ return i * 8 + ((7 - ((Math.log(b) / Math.LN2) | 0)) | 0);
+};
+
+const xor = (a: Uint8Array, b: Uint8Array): Uint8Array => {
+ const c = Buffer.alloc(Math.min(a.byteLength, b.byteLength));
+ for (let i = 0; i < c.byteLength; i++) c[i] = a[i] ^ b[i];
+ return c;
+};
+
+export class ID {
+ publicKey: Uint8Array = Buffer.alloc(nacl.SignLength.PublicKey);
+ host: IPv4 | IPv6;
+ port: number = 0;
+
+ constructor(publicKey: Uint8Array, host: IPv4 | IPv6, port: number) {
+ this.publicKey = publicKey;
+ this.host = host;
+ this.port = port;
+ }
+
+ get addr(): string {
+ let host = this.host;
+ if (host.kind() === "ipv6" && (host).isIPv4MappedAddress()) {
+ host = (host).toIPv4Address();
+ }
+ return host.toString() + ":" + this.port;
+ }
+
+ public encode(): Buffer {
+ let host = Buffer.of(...this.host.toByteArray());
+ host = Buffer.concat([Buffer.of(host.byteLength === 4 ? 0 : 1), host]);
+
+ const port = Buffer.alloc(2);
+ port.writeUInt16BE(this.port);
+
+ return Buffer.concat([this.publicKey, host, port]);
+ }
+
+ public static decode(buf: Buffer): [ID, Buffer] {
+ const publicKey = Uint8Array.from(buf.slice(0, nacl.SignLength.PublicKey));
+ buf = buf.slice(nacl.SignLength.PublicKey);
+
+ const hostHeader = buf.readUInt8();
+ buf = buf.slice(1);
+
+ assert(hostHeader === 0 || hostHeader === 1);
+
+ const hostLen = hostHeader === 0 ? 4 : 16;
+ const host = ipaddr.fromByteArray([...buf.slice(0, hostLen)]);
+ buf = buf.slice(hostLen);
+
+ const port = buf.readUInt16BE();
+ buf = buf.slice(2);
+
+ return [new ID(publicKey, host, port), buf];
+ }
+}
+
+export class Table {
+ buckets: Array> = [
+ ...Array(nacl.SignLength.PublicKey * 8),
+ ].map(() => []);
+
+ pub: Uint8Array;
+ cap: number = 16;
+ length: number = 0;
+
+ public constructor(
+ pub: Uint8Array = Buffer.alloc(nacl.SignLength.PublicKey)
+ ) {
+ this.pub = pub;
+ }
+
+ private bucketIndex(pub: Uint8Array): number {
+ if (BufferCompare(pub, this.pub) === 0) return 0;
+ return leadingZeros(xor(pub, this.pub));
+ }
+
+ public update(id: ID): UpdateResult {
+ if (BufferCompare(id.publicKey, this.pub) === 0) return UpdateResult.Fail;
+
+ const bucket = this.buckets[this.bucketIndex(id.publicKey)];
+
+ const i = bucket.findIndex(
+ (item) => BufferCompare(item.publicKey, id.publicKey) === 0
+ );
+ if (i >= 0) {
+ bucket.unshift(...bucket.splice(i, 1));
+ return UpdateResult.Ok;
+ }
+
+ if (bucket.length < this.cap) {
+ bucket.unshift(id);
+ this.length++;
+ return UpdateResult.New;
+ }
+ return UpdateResult.Full;
+ }
+
+ public delete(pub: Uint8Array): boolean {
+ const bucket = this.buckets[this.bucketIndex(pub)];
+ const i = bucket.findIndex((id) => BufferCompare(id.publicKey, pub) === 0);
+ if (i >= 0) {
+ bucket.splice(i, 1);
+ this.length--;
+ return true;
+ }
+ return false;
+ }
+
+ public has(pub: Uint8Array): boolean {
+ const bucket = this.buckets[this.bucketIndex(pub)];
+ return !!bucket.find((id) => BufferCompare(id.publicKey, pub) === 0);
+ }
+
+ public closestTo(pub: Uint8Array, k = this.cap): ID[] {
+ const closest: ID[] = [];
+
+ const fill = (i: number) => {
+ const bucket = this.buckets[i];
+ for (let i = 0; closest.length < k && i < bucket.length; i++) {
+ if (BufferCompare(bucket[i].publicKey, pub) != 0)
+ closest.push(bucket[i]);
+ }
+ };
+
+ const m = this.bucketIndex(pub);
+
+ fill(m);
+
+ for (
+ let i = 1;
+ closest.length < k && (m - i >= 0 || m + i < this.buckets.length);
+ i++
+ ) {
+ if (m - i >= 0) fill(m - i);
+ if (m + i < this.buckets.length) fill(m + i);
+ }
+
+ closest.sort((a: ID, b: ID) =>
+ BufferCompare(xor(a.publicKey, pub), xor(b.publicKey, pub))
+ );
+
+ return closest.length > k ? closest.slice(0, k) : closest;
+ }
+}
diff --git a/deno/src/mod.ts b/deno/src/mod.ts
new file mode 100644
index 0000000..8521f61
--- /dev/null
+++ b/deno/src/mod.ts
@@ -0,0 +1,14 @@
+export { Node, generateSecretKey } from "./node.ts";
+export { Context } from "./context.ts";
+export { ID, Table, UpdateResult } from "./kademlia.ts";
+export { getAvailableAddress, splitHostPort } from "./net.ts";
+export { Provider } from "./provider.ts";
+export { x25519, serverHandshake, clientHandshake, Session } from "./session.ts";
+export {
+ drain,
+ lengthPrefixed,
+ prefixLength,
+ RPC,
+ Stream,
+ Streams,
+} from "./stream.ts";
diff --git a/deno/src/net.ts b/deno/src/net.ts
new file mode 100644
index 0000000..36da440
--- /dev/null
+++ b/deno/src/net.ts
@@ -0,0 +1,50 @@
+import { IPv4, IPv6 } from "https://jspm.dev/ipaddr.js";
+import * as net from "./std-node-net.ts";
+import * as events from "https://deno.land/std/node/events.ts";
+import ipaddr from "https://jspm.dev/ipaddr.js";
+
+/**
+ * Returns an available TCP host/port that may be listened to.
+ */
+export async function getAvailableAddress(): Promise<{
+ family: string;
+ host: IPv4 | IPv6;
+ port: number;
+}> {
+ const server = net.createServer();
+ server.unref();
+ server.listen();
+
+ await events.once(server, "listening");
+
+ const info = (server.address())!;
+
+ let host = ipaddr.parse(info.address.length === 0 ? "0.0.0.0" : info.address);
+ if (host.kind() === "ipv6" && (host).isIPv4MappedAddress()) {
+ host = (host).toIPv4Address();
+ }
+
+ server.close();
+
+ await events.once(server, "close");
+
+ return { family: info.family, host, port: info.port };
+}
+
+export function splitHostPort(
+ addr: string
+): { host: IPv4 | IPv6; port: number } {
+ const fields = addr.split(":").filter((field) => field.length > 0);
+ if (fields.length === 0)
+ throw new Error("Unable to split host:port from address.");
+
+ const port = parseInt(fields.pop()!);
+ if (port < 0 || port > 2 ** 16) throw new Error(`Port ${port} is invalid.`);
+
+ let host = ipaddr.parse(fields.length === 0 ? "0.0.0.0" : fields.join(":"));
+ if (host.kind() === "ipv6" && (host).isIPv4MappedAddress()) {
+ host = (host).toIPv4Address();
+ }
+
+ return { host, port };
+}
diff --git a/deno/src/node.ts b/deno/src/node.ts
new file mode 100644
index 0000000..f0057d8
--- /dev/null
+++ b/deno/src/node.ts
@@ -0,0 +1,620 @@
+import { Buffer } from "https://deno.land/std/node/buffer.ts";
+import { debug } from "https://deno.land/std/log/mod.ts";
+import { Context, Handler } from "./context.ts";
+import * as net from "./std-node-net.ts";
+import { ID, Table } from "./kademlia.ts";
+import * as nacl from 'https://deno.land/x/tweetnacl_deno/src/nacl.ts'
+import { getAvailableAddress, splitHostPort } from "./net.ts";
+// import ipaddr from "https://jspm.dev/ipaddr.js";
+import {
+ DataPacket,
+ FindNodeRequest,
+ FindNodeResponse,
+ HandshakePacket,
+ Opcode,
+ ServiceRequestPacket,
+ ServiceResponsePacket,
+} from "./packet.ts";
+import * as events from "https://deno.land/std/node/events.ts";
+import { clientHandshake, serverHandshake, Session } from "./session.ts";
+import { Provider } from "./provider.ts";
+import hash from "https://jspm.dev/object-hash";
+
+export interface NodeOptions {
+ // A reachable, public address which peers may reach you on.
+ // The format of the address must be [host]:[port].
+ publicAddr?: string;
+
+ // A list of [host]:[port] addresses which this node will bind a listener
+ // against to accept new Flatend nodes.
+ bindAddrs?: string[];
+
+ // A list of addresses to nodes to initially reach out
+ // for/bootstrap from first.
+ addrs?: string[];
+
+ // An Ed25519 secret key. A secret key must be provided to allow for
+ // peers to reach you. A secret key may be generated by calling
+ // 'flatend.generateSecretKey()'.
+ secretKey?: Uint8Array;
+
+ // A mapping of service names to their respective handlers.
+ services?: { [key: string]: Handler };
+}
+
+export class Node {
+ services = new Map>();
+ clients = new Map();
+ servers = new Set();
+ conns = new Set();
+ table = new Table();
+
+ id?: ID;
+ keys?: nacl.SignKeyPair;
+ handlers: { [key: string]: Handler } = {};
+ _shutdown = false;
+
+ public static async start(opts: NodeOptions): Promise {
+ const node = new Node();
+
+ if (opts.services) node.handlers = opts.services;
+
+ if (opts.secretKey) {
+ node.keys = nacl.sign_keyPair_fromSecretKey(opts.secretKey);
+
+ debug(`Public Key: ${Buffer.from(node.keys.publicKey).toString("hex")}`);
+
+ const bindAddrs = opts.bindAddrs ?? [];
+ if (bindAddrs.length === 0) {
+ if (opts.publicAddr) {
+ bindAddrs.push(opts.publicAddr);
+ } else {
+ const { host, port } = await getAvailableAddress();
+ bindAddrs.push(host + ":" + port);
+ }
+ }
+
+ // TODO: import from ipaddr
+ // let publicHost: ipaddr.IPv4 | ipaddr.IPv6;
+ let publicHost: any;
+ let publicPort: number;
+
+ if (opts.publicAddr) {
+ const { host, port } = splitHostPort(opts.publicAddr);
+ publicHost = host;
+ publicPort = port;
+ } else {
+ const { host, port } = splitHostPort(bindAddrs[0]);
+ publicHost = host;
+ publicPort = port;
+ }
+
+ node.id = new ID(node.keys.publicKey, publicHost, publicPort);
+ node.table = new Table(node.id.publicKey);
+
+ const promises = [];
+
+ for (const bindAddr of bindAddrs) {
+ const { host, port } = splitHostPort(bindAddr);
+ promises.push(node.listen({ host: host.toString(), port }));
+ }
+
+ await Promise.all(promises);
+ }
+
+ if (opts.addrs) {
+ const promises = [];
+
+ for (const addr of opts.addrs) {
+ const { host, port } = splitHostPort(addr);
+ promises.push(node.connect({ host: host.toString(), port: port }));
+ }
+
+ await Promise.all(promises);
+ await node.bootstrap();
+ }
+
+ return node;
+ }
+
+ async bootstrap() {
+ const pub = this.id?.publicKey ?? Buffer.alloc(nacl.SignLength.PublicKey);
+ const visited = new Set();
+
+ let queue: ID[] = this.table.closestTo(pub, this.table.cap);
+ if (queue.length === 0) return;
+
+ for (const id of queue) {
+ visited.add(Buffer.from(id.publicKey).toString("hex"));
+ }
+
+ const closest: ID[] = [];
+
+ while (queue.length > 0) {
+ const next: ID[] = [];
+
+ await Promise.all(
+ queue.map(async (id) => {
+ const { host, port } = splitHostPort(id.addr);
+
+ try {
+ const client = await this.connect({ host: host.toString(), port });
+
+ const res = FindNodeResponse.decode(
+ await client.request(
+ Buffer.concat([
+ Buffer.of(Opcode.FindNodeRequest),
+ new FindNodeRequest(pub).encode(),
+ ])
+ )
+ )[0];
+
+ res.closest = res.closest.filter((id) => {
+ return !visited.has(Buffer.from(id.publicKey).toString("hex"));
+ });
+
+ closest.push(...res.closest);
+ next.push(...res.closest);
+ } catch (err) {
+ // ignore
+ }
+ })
+ );
+
+ queue = next;
+ }
+
+ debug(`Discovered ${closest.length} peer(s).`);
+ }
+
+ /**
+ * Shuts down all active connections and listeners on this node. After shutting
+ * down a node, it may not be reused. \
+ */
+ async shutdown() {
+ if (this._shutdown) throw new Error("Node is shut down.");
+
+ this._shutdown = true;
+
+ const promises = [];
+
+ for (const conn of this.conns) {
+ promises.push(events.once(conn, "close"));
+ conn.end();
+ }
+
+ for (const server of this.servers) {
+ promises.push(events.once(server, "close"));
+ server.close();
+ }
+
+ await Promise.all(promises);
+ }
+
+ /**
+ * Provides a list of nodes that provide either one of the many specified services.
+ *
+ * @param services List of services.
+ */
+ providersFor(services: string[]): Provider[] {
+ const map = this._providers(services).reduce(
+ (map: Map, provider: Provider) =>
+ provider.id?.publicKey
+ ? map.set(hash(provider.id.publicKey), provider)
+ : map,
+ new Map()
+ );
+
+ return [...map.values()];
+ }
+
+ _providers(services: string[]): Provider[] {
+ const providers: Provider[] = [];
+ for (const service of services) {
+ const entries = this.services.get(service);
+ if (!entries) continue;
+ providers.push(...entries);
+ }
+ return providers;
+ }
+
+ /**
+ * Request one of any available nodes to provide one of the many specified services. A request header
+ * may be attached to the request sent out to a designated node, along with a body.
+ *
+ * @param services List of services.
+ * @param headers Request headers.
+ * @param body The request body. Must not be null/undefined.
+ */
+ async push(
+ services: string[],
+ headers: { [key: string]: string },
+ body: AsyncIterable
+ ) {
+ if (this._shutdown) throw new Error("Node is shut down.");
+
+ const providers = this._providers(services);
+
+ for (const provider of providers) {
+ return await provider.push(services, headers, body);
+ }
+
+ throw new Error(
+ `No nodes were able to process your request for service(s): [${services.join(
+ ", "
+ )}]`
+ );
+ }
+
+ /**
+ * Start listening for Flatend nodes at a specified IP family/host/port.
+ *
+ * @param opts IP family/host/port.
+ */
+ async listen(opts: net.ListenOptions) {
+ if (this._shutdown) throw new Error("Node is shut down.");
+
+ const server = net.createServer(async (conn) => {
+ this.conns.add(conn);
+
+ setTimeout(async () => {
+ await events.once(conn, "close");
+ this.conns.delete(conn);
+ });
+
+ try {
+ const secret = await serverHandshake(conn);
+ const session = new Session(secret);
+
+ const provider = new Provider(conn, session, false);
+ setTimeout(() => this.read(provider));
+ } catch (err) {
+ debug("Error from incoming node:", err);
+ conn.end();
+ }
+ });
+
+ server.listen(opts);
+
+ await events.once(server, "listening");
+
+ this.servers.add(server);
+
+ setTimeout(async () => {
+ await events.once(server, "close");
+ this.servers.delete(server);
+ });
+
+ const info = (server.address())!;
+
+ debug(`Listening for Flatend nodes on '${info.address}:${info.port}'.`);
+ }
+
+ /**
+ * Connect to a Flatend node and ask and keep track of the services it provides.
+ *
+ * @param opts Flatend node IP family/host/port.
+ */
+ async connect(opts: net.NetConnectOpts) {
+ if (this._shutdown) throw new Error("Node is shut down.");
+
+ let provider = this.clients.get(hash(opts));
+
+ if (!provider) {
+ const conn = net.connect(opts);
+ await events.once(conn, "connect");
+ this.conns.add(conn);
+
+ setTimeout(async () => {
+ await events.once(conn, "close");
+ this.clients.delete(hash(opts));
+ this.conns.delete(conn);
+ });
+
+ try {
+console.log('connect beforehad')
+ const secret = await clientHandshake(conn);
+console.log('connect aftershake')
+ const session = new Session(secret);
+console.log('connect aftersess')
+
+ provider = new Provider(conn, session, true);
+ this.clients.set(hash(opts), provider);
+
+ setTimeout(() => this.read(provider!));
+
+ const handshake = new HandshakePacket(
+ this.id,
+ [...Object.keys(this.handlers)],
+ undefined
+ );
+ if (this.keys)
+ handshake.signature = nacl.sign_detached(
+ handshake.payload,
+ this.keys.secretKey
+ );
+
+ const response = await provider.request(
+ Buffer.concat([Buffer.of(Opcode.Handshake), handshake.encode()])
+ );
+ const packet = HandshakePacket.decode(response)[0];
+
+ provider.handshaked = true;
+
+ if (packet.id && packet.signature) {
+ if (
+ !nacl.sign_detached_verify(
+ packet.payload,
+ packet.signature,
+ packet.id.publicKey
+ )
+ ) {
+ throw new Error(`Handshake packet signature is malformed.`);
+ }
+ provider.id = packet.id;
+ this.table.update(provider.id);
+ }
+
+ debug(
+ `You have connected to '${
+ provider.addr
+ }'. Services: [${packet.services.join(", ")}]`
+ );
+
+ for (const service of packet.services) {
+ provider.services.add(service);
+
+ let providers = this.services.get(service);
+ if (!providers) {
+ providers = new Set();
+ this.services.set(service, providers);
+ }
+ providers.add(provider);
+ }
+
+ setTimeout(async () => {
+ await events.once(provider!.sock, "end");
+
+ debug(
+ `'${
+ provider!.addr
+ }' has disconnected from you. Services: [${packet.services.join(
+ ", "
+ )}]`
+ );
+
+ if (provider!.id) {
+ this.table.delete(provider!.id.publicKey);
+ }
+
+ for (const service of packet.services) {
+ let providers = this.services.get(service)!;
+ if (!providers) continue;
+
+ providers.delete(provider!);
+ if (providers.size === 0) this.services.delete(service);
+ }
+ });
+
+ setTimeout(async () => {
+ await events.once(provider!.sock, "end");
+
+ if (this._shutdown) return;
+
+ let count = 8;
+
+ const reconnect = async () => {
+ if (this._shutdown) return;
+
+ if (count-- === 0) {
+ debug(
+ `Tried 8 times reconnecting to ${provider!.addr}. Giving up.`
+ );
+ return;
+ }
+
+ debug(
+ `Trying to reconnect to '${provider!.addr}'. Sleeping for 500ms.`
+ );
+
+ try {
+ await this.connect(opts);
+ } catch (err) {
+ setTimeout(reconnect, 500);
+ }
+ };
+
+ setTimeout(reconnect, 500);
+ });
+ } catch (err) {
+ conn.end();
+ throw err;
+ }
+ }
+
+ return provider;
+ }
+
+ async read(provider: Provider) {
+ try {
+ await this._read(provider);
+ } catch (err) {
+ debug("Provider had shut down with an error:", err);
+ }
+
+ provider.sock.end();
+ }
+
+ async _read(provider: Provider) {
+ for await (const { seq, opcode, frame } of provider.read()) {
+ await this._handle(provider, seq, opcode, frame);
+ }
+ }
+
+ async _handle(
+ provider: Provider,
+ seq: number,
+ opcode: number,
+ frame: Buffer
+ ) {
+ switch (opcode) {
+ case Opcode.Handshake: {
+ if (provider.handshaked) {
+ throw new Error("Provider attempted to handshake twice.");
+ }
+ provider.handshaked = true;
+
+ const packet = HandshakePacket.decode(frame)[0];
+ if (packet.id && packet.signature) {
+ if (
+ !nacl.sign_detached_verify(
+ packet.payload,
+ packet.signature,
+ packet.id.publicKey
+ )
+ ) {
+ throw new Error(`Handshake packet signature is malformed.`);
+ }
+ provider.id = packet.id;
+ this.table.update(provider.id);
+ }
+
+ debug(
+ `'${
+ provider.addr
+ }' has connected to you. Services: [${packet.services.join(", ")}]`
+ );
+
+ for (const service of packet.services) {
+ provider.services.add(service);
+
+ let providers = this.services.get(service);
+ if (!providers) {
+ providers = new Set();
+ this.services.set(service, providers);
+ }
+ providers.add(provider);
+ }
+
+ setTimeout(async () => {
+ await events.once(provider.sock, "end");
+
+ debug(
+ `'${
+ provider.addr
+ }' has disconnected from you. Services: [${packet.services.join(
+ ", "
+ )}]`
+ );
+
+ if (provider.id) {
+ this.table.delete(provider.id.publicKey);
+ }
+
+ for (const service of packet.services) {
+ let providers = this.services.get(service)!;
+ if (!providers) continue;
+
+ providers.delete(provider);
+ if (providers.size === 0) this.services.delete(service);
+ }
+ });
+
+ const response = new HandshakePacket(
+ this.id,
+ [...Object.keys(this.handlers)],
+ undefined
+ );
+ if (this.keys)
+ response.signature = nacl.sign_detached(
+ response.payload,
+ this.keys.secretKey
+ );
+
+ await provider.write(provider.rpc.message(seq, response.encode()));
+
+ return;
+ }
+ case Opcode.ServiceRequest: {
+ const packet = ServiceRequestPacket.decode(frame)[0];
+ const stream = provider.streams.register(packet.id);
+
+ const service = packet.services.find(
+ (service) => service in this.handlers
+ );
+ if (!service) {
+ const payload = new ServiceResponsePacket(
+ packet.id,
+ false,
+ {}
+ ).encode();
+ await provider.write(
+ provider.rpc.message(
+ 0,
+ Buffer.concat([Buffer.of(Opcode.ServiceResponse), payload])
+ )
+ );
+ } else {
+ const ctx = new Context(provider, stream, packet.headers);
+ const handler = this.handlers[service];
+
+ setTimeout(async () => {
+ try {
+ await handler(ctx);
+ } catch (err) {
+ if (!ctx.writableEnded) {
+ ctx.json({ error: err?.message ?? "Internal server error." });
+ }
+ }
+ });
+ }
+
+ return;
+ }
+ case Opcode.ServiceResponse: {
+ const packet = ServiceResponsePacket.decode(frame)[0];
+ const stream = provider.streams.get(packet.id);
+ if (!stream) {
+ throw new Error(
+ `Got response headers for stream ID ${packet.id} which is not registered.`
+ );
+ }
+ provider.streams.pull(stream, packet.handled, packet.headers);
+ return;
+ }
+ case Opcode.Data: {
+ const packet = DataPacket.decode(frame)[0];
+ const stream = provider.streams.get(packet.id);
+ if (!stream) {
+ throw new Error(
+ `Got data for stream ID ${packet.id} which is not registered, or has ended.`
+ );
+ }
+ provider.streams.recv(stream, packet.data);
+ return;
+ }
+ case Opcode.FindNodeRequest: {
+ const packet = FindNodeRequest.decode(frame)[0];
+ const response = new FindNodeResponse(
+ this.table.closestTo(packet.target, this.table.cap)
+ );
+ await provider.write(provider.rpc.message(seq, response.encode()));
+ }
+ }
+ }
+}
+
+/**
+ * Generates an Ed25519 secret key for a node.
+ */
+export function generateSecretKey(): Buffer {
+ return Buffer.from(nacl.sign_keyPair().secretKey);
+}
+
+export function* chunkBuffer(buf: Buffer, size: number) {
+ while (buf.byteLength > 0) {
+ size = size > buf.byteLength ? buf.byteLength : size;
+ yield buf.slice(0, size);
+ buf = buf.slice(size);
+ }
+}
diff --git a/deno/src/packet.ts b/deno/src/packet.ts
new file mode 100644
index 0000000..61efc61
--- /dev/null
+++ b/deno/src/packet.ts
@@ -0,0 +1,333 @@
+import { Buffer } from "https://deno.land/std/node/buffer.ts";
+import * as nacl from "https://deno.land/x/tweetnacl_deno/src/nacl.ts";
+import {assert} from "https://deno.land/std/testing/asserts.ts";
+import { ID } from "./kademlia.ts";
+
+export enum Opcode {
+ Handshake,
+ ServiceRequest,
+ ServiceResponse,
+ Data,
+ FindNodeRequest,
+ FindNodeResponse,
+}
+
+export class HandshakePacket {
+ id?: ID;
+ services: string[] = [];
+ signature?: Uint8Array;
+
+ constructor(
+ id: ID | undefined,
+ services: string[],
+ signature: Uint8Array | undefined
+ ) {
+ this.id = id;
+ this.services = services;
+ this.signature = signature;
+ }
+
+ get payload() {
+ return Buffer.concat([
+ this.id!.encode(),
+ ...this.services.map((service) => Buffer.from(service, "utf8")),
+ ]);
+ }
+
+ public encode(): Buffer {
+ const id = this.id
+ ? Buffer.concat([Buffer.of(1), this.id.encode()])
+ : Buffer.of(0);
+ const services = this.services.reduce(
+ (result, service) =>
+ Buffer.concat([
+ result,
+ Buffer.of(service.length),
+ Buffer.from(service, "utf8"),
+ ]),
+ Buffer.of(this.services.length)
+ );
+ const signature = this.id && this.signature ? this.signature : Buffer.of();
+
+ return Buffer.concat([id, services, signature]);
+ }
+
+ public static decode(buf: Buffer): [HandshakePacket, Buffer] {
+ const header = buf.readUInt8();
+ buf = buf.slice(1);
+
+ assert(header === 0 || header === 1);
+
+ const result: [ID | undefined, Buffer] =
+ header === 1 ? ID.decode(buf) : [undefined, buf];
+
+ const id = result[0];
+ buf = result[1];
+
+ const size = buf.readUInt8();
+ buf = buf.slice(1);
+
+ const services: string[] = [...Array(size)].map(() => {
+ const size = buf.readUInt8();
+ buf = buf.slice(1);
+
+ const service = buf.slice(0, size);
+ buf = buf.slice(size);
+
+ return service.toString("utf8");
+ });
+
+ let signature: Buffer | undefined;
+ if (id) {
+ signature = buf.slice(0, nacl.SignLength.Signature);
+ buf = buf.slice(nacl.SignLength.Signature);
+ }
+
+ return [new HandshakePacket(id, services, signature), buf];
+ }
+}
+
+export class ServiceRequestPacket {
+ id: number;
+ services: string[] = [];
+ headers: { [key: string]: string };
+
+ public constructor(
+ id: number,
+ services: string[],
+ headers: { [key: string]: string }
+ ) {
+ this.id = id;
+ this.services = services;
+ this.headers = headers;
+ }
+
+ public encode(): Buffer {
+ const id = Buffer.alloc(4);
+ id.writeUInt32BE(this.id);
+
+ const services = this.services.reduce(
+ (result, service) =>
+ Buffer.concat([
+ result,
+ Buffer.of(service.length),
+ Buffer.from(service, "utf8"),
+ ]),
+ Buffer.of(this.services.length)
+ );
+
+ const headersLen = Buffer.alloc(2);
+ headersLen.writeUInt16BE(Object.keys(this.headers).length);
+
+ const headers = Object.keys(this.headers).reduce((result, key) => {
+ const value = this.headers[key];
+
+ const keyBuf = Buffer.concat([
+ Buffer.of(key.length),
+ Buffer.from(key, "utf8"),
+ ]);
+ const valueBuf = Buffer.concat([
+ Buffer.alloc(2),
+ Buffer.from(value, "utf8"),
+ ]);
+ valueBuf.writeUInt16BE(value.length);
+
+ return Buffer.concat([result, keyBuf, valueBuf]);
+ }, headersLen);
+
+ return Buffer.concat([id, services, headers]);
+ }
+
+ public static decode(buf: Buffer): [ServiceRequestPacket, Buffer] {
+ const id = buf.readUInt32BE();
+ buf = buf.slice(4);
+
+ const servicesLen = buf.readUInt8();
+ buf = buf.slice(1);
+
+ const services: string[] = [...Array(servicesLen)].map(() => {
+ const serviceLen = buf.readUInt8();
+ buf = buf.slice(1);
+
+ const service = buf.slice(0, serviceLen);
+ buf = buf.slice(serviceLen);
+
+ return service.toString("utf8");
+ });
+
+ const headersLen = buf.readUInt16BE();
+ buf = buf.slice(2);
+
+ const headers = [...Array(headersLen)].reduce((map, _) => {
+ const keyLen = buf.readUInt8();
+ buf = buf.slice(1);
+
+ const key = buf.slice(0, keyLen).toString("utf8");
+ buf = buf.slice(keyLen);
+
+ const valueLen = buf.readUInt16BE();
+ buf = buf.slice(2);
+
+ const value = buf.slice(0, valueLen).toString("utf8");
+ buf = buf.slice(valueLen);
+
+ map[key] = value;
+ return map;
+ }, {});
+
+ return [new ServiceRequestPacket(id, services, headers), buf];
+ }
+}
+
+export class ServiceResponsePacket {
+ id: number;
+ handled: boolean = false;
+ headers: { [key: string]: string };
+
+ public constructor(
+ id: number,
+ handled: boolean,
+ headers: { [key: string]: string }
+ ) {
+ this.id = id;
+ this.handled = handled;
+ this.headers = headers;
+ }
+
+ public encode(): Buffer {
+ const id = Buffer.alloc(4);
+ id.writeUInt32BE(this.id);
+
+ const handled = Buffer.of(this.handled ? 1 : 0);
+
+ const headersLen = Buffer.alloc(2);
+ headersLen.writeUInt16BE(Object.keys(this.headers).length);
+
+ const headers = Object.keys(this.headers).reduce((result, key) => {
+ const value = this.headers[key];
+
+ const keyBuf = Buffer.concat([
+ Buffer.of(key.length),
+ Buffer.from(key, "utf8"),
+ ]);
+ const valueBuf = Buffer.concat([
+ Buffer.alloc(2),
+ Buffer.from(value, "utf8"),
+ ]);
+ valueBuf.writeUInt16BE(value.length);
+
+ return Buffer.concat([result, keyBuf, valueBuf]);
+ }, headersLen);
+
+ return Buffer.concat([id, handled, headers]);
+ }
+
+ public static decode(buf: Buffer): [ServiceResponsePacket, Buffer] {
+ const id = buf.readUInt32BE();
+ buf = buf.slice(4);
+
+ const handled = buf.readUInt8() === 1;
+ buf = buf.slice(1);
+
+ const headersLen = buf.readUInt16BE();
+ buf = buf.slice(2);
+
+ const headers = [...Array(headersLen)].reduce((map, _) => {
+ const keyLen = buf.readUInt8();
+ buf = buf.slice(1);
+
+ const key = buf.slice(0, keyLen).toString("utf8");
+ buf = buf.slice(keyLen);
+
+ const valueLen = buf.readUInt16BE();
+ buf = buf.slice(2);
+
+ const value = buf.slice(0, valueLen).toString("utf8");
+ buf = buf.slice(valueLen);
+
+ map[key] = value;
+ return map;
+ }, {});
+
+ return [new ServiceResponsePacket(id, handled, headers), buf];
+ }
+}
+
+export class DataPacket {
+ id: number;
+ data: Buffer;
+
+ public constructor(id: number, data: Buffer) {
+ this.id = id;
+ this.data = data;
+ }
+
+ public encode(): Buffer {
+ const id = Buffer.alloc(4);
+ id.writeUInt32BE(this.id);
+
+ const dataLen = Buffer.alloc(2);
+ dataLen.writeUInt16BE(this.data.byteLength);
+
+ return Buffer.concat([id, dataLen, this.data]);
+ }
+
+ public static decode(buf: Buffer): [DataPacket, Buffer] {
+ const id = buf.readUInt32BE();
+ buf = buf.slice(4);
+
+ const dataLen = buf.readUInt16BE();
+ buf = buf.slice(2);
+
+ const data = buf.slice(0, dataLen);
+ buf = buf.slice(dataLen);
+
+ return [new DataPacket(id, data), buf];
+ }
+}
+
+export class FindNodeRequest {
+ target: Uint8Array;
+
+ public constructor(target: Uint8Array) {
+ this.target = target;
+ }
+
+ public encode(): Buffer {
+ return Buffer.from(this.target);
+ }
+
+ public static decode(buf: Buffer): [FindNodeRequest, Buffer] {
+ const target = buf.slice(0, nacl.SignLength.PublicKey);
+ buf = buf.slice(nacl.SignLength.PublicKey);
+ return [new FindNodeRequest(target), buf];
+ }
+}
+
+export class FindNodeResponse {
+ closest: ID[];
+
+ public constructor(closest: ID[]) {
+ this.closest = closest;
+ }
+
+ public encode(): Buffer {
+ return Buffer.concat([
+ Buffer.of(this.closest.length),
+ ...this.closest.map((id) => id.encode()),
+ ]);
+ }
+
+ public static decode(buf: Buffer): [FindNodeResponse, Buffer] {
+ const closestLen = buf.readUInt8();
+ buf = buf.slice(1);
+
+ const closest = [...Array(closestLen)].map(() => {
+ const [id, leftover] = ID.decode(buf);
+ buf = leftover;
+ return id;
+ });
+
+ return [new FindNodeResponse(closest), buf];
+ }
+}
diff --git a/deno/src/provider.ts b/deno/src/provider.ts
new file mode 100644
index 0000000..9152e65
--- /dev/null
+++ b/deno/src/provider.ts
@@ -0,0 +1,96 @@
+import { Buffer } from "https://deno.land/std/node/buffer.ts";
+import { ID } from "./kademlia.ts";
+import * as net from "./std-node-net.ts";
+import { Session } from "./session.ts";
+import {
+ drain,
+ lengthPrefixed,
+ prefixLength,
+ RPC,
+ Stream,
+ Streams,
+} from "./stream.ts";
+import { Opcode } from "./packet.ts";
+
+export class Provider {
+ id?: ID;
+ handshaked = false;
+ services = new Set();
+
+ sock: net.Socket;
+ session: Session;
+ rpc: RPC;
+ streams: Streams;
+
+ get addr(): string {
+ if (this.id) return this.id.addr;
+ return "";
+ }
+
+ constructor(sock: net.Socket, session: Session, client: boolean) {
+ this.sock = sock;
+ this.session = session;
+ this.rpc = new RPC(client);
+ this.streams = new Streams(client);
+ }
+
+ async write(buf: Buffer) {
+ buf = prefixLength(this.session.encrypt(buf));
+ if (!this.sock.write(buf)) await drain(this.sock);
+ }
+
+ async request(data: Buffer): Promise {
+ const [req, res] = this.rpc.request(data);
+ await this.write(req);
+ return await res;
+ }
+
+ async *read() {
+ const stream = this.rpc.parse(
+ this.session.decrypted(lengthPrefixed(this.sock))
+ );
+ for await (let { seq, frame } of stream) {
+ if (frame.byteLength < 1)
+ throw new Error(`Frame must be prefixed with an opcode byte.`);
+
+ const opcode = frame.readUInt8();
+ frame = frame.slice(1);
+
+ yield { seq, opcode, frame };
+ }
+ }
+
+ async push(
+ services: string[],
+ headers: { [key: string]: string },
+ body: AsyncIterable
+ ): Promise {
+ const err = new Error(
+ `No nodes were able to process your request for service(s): [${services.join(
+ ", "
+ )}]`
+ );
+
+ const stream = this.streams.register();
+ const [header, handled] = this.streams.push(stream, services, headers);
+
+ await this.write(
+ this.rpc.message(
+ 0,
+ Buffer.concat([Buffer.of(Opcode.ServiceRequest), header])
+ )
+ );
+
+ for await (const chunk of this.streams.encoded(stream.id, body)) {
+ await this.write(
+ this.rpc.message(0, Buffer.concat([Buffer.of(Opcode.Data), chunk]))
+ );
+ }
+
+ if (!(await handled)) {
+ throw err;
+ }
+
+ return stream;
+ }
+}
diff --git a/deno/src/session.ts b/deno/src/session.ts
new file mode 100644
index 0000000..ac28626
--- /dev/null
+++ b/deno/src/session.ts
@@ -0,0 +1,88 @@
+import { Buffer } from "https://deno.land/std/node/buffer.ts";
+import * as net from "./std-node-net.ts";
+import * as nacl from "https://deno.land/x/tweetnacl_deno/src/nacl.ts";
+import * as events from "https://deno.land/std/node/events.ts";
+import crypto from "./std-node-crypto.ts";
+
+import { blake2b } from "https://deno.land/x/blake2b/mod.ts";
+
+export function x25519(privateKey: Uint8Array, publicKey: Uint8Array): Buffer {
+ return Buffer.from(blake2b(nacl.scalarMult(privateKey, publicKey), undefined, undefined, 32).toString());
+}
+
+export async function serverHandshake(conn: net.Socket): Promise {
+ const serverKeys = nacl.box_keyPair();
+
+ await events.once(conn, "readable");
+ const clientPublicKey = conn.read(nacl.BoxLength.PublicKey);
+
+ conn.write(serverKeys.publicKey);
+
+ return x25519(serverKeys.secretKey, clientPublicKey);
+}
+
+export async function clientHandshake(client: net.Socket): Promise {
+ const clientKeys = nacl.box_keyPair();
+
+ client.write(clientKeys.publicKey);
+console.log('hanread')
+ await events.once(client, "readable");
+ const serverPublicKey = client.read(nacl.BoxLength.PublicKey);
+
+console.log('handpuh', serverPublicKey)
+ return x25519(clientKeys.secretKey, serverPublicKey);
+}
+
+export class Session {
+ secret: Uint8Array;
+ readNonce: bigint = BigInt(0);
+ writeNonce: bigint = BigInt(0);
+
+ constructor(secret: Uint8Array) {
+ this.secret = secret;
+ }
+
+ public async *decrypted(stream: AsyncIterable) {
+ for await (const frame of stream) {
+ yield this.decrypt(frame);
+ }
+ }
+
+ public encrypt(src: string | ArrayBufferLike): Buffer {
+ // @ts-ignore
+ const buf = Buffer.isBuffer(src) ? src : Buffer.from(src);
+
+ const nonce = Buffer.alloc(12);
+ nonce.writeBigUInt64BE(this.writeNonce);
+ this.writeNonce = this.writeNonce + BigInt(1);
+
+ const cipher = crypto.createCipheriv("aes-256-gcm", this.secret, nonce, {
+ authTagLength: 16,
+ });
+ const ciphered = cipher.update(buf);
+ cipher.final();
+
+ return Buffer.concat([ciphered, cipher.getAuthTag()]);
+ }
+
+ public decrypt(buf: Buffer): Buffer {
+ if (buf.byteLength < 16) throw new Error("Missing authentication tag.");
+
+ const nonce = Buffer.alloc(12);
+ nonce.writeBigUInt64BE(this.readNonce);
+ this.readNonce = this.readNonce + BigInt(1);
+
+ const decipher = crypto.createDecipheriv(
+ "aes-256-gcm",
+ this.secret,
+ nonce,
+ { authTagLength: 16 }
+ );
+ decipher.setAuthTag(buf.slice(buf.byteLength - 16, buf.byteLength));
+
+ const deciphered = decipher.update(buf.slice(0, buf.byteLength - 16));
+ decipher.final();
+
+ return deciphered;
+ }
+}
diff --git a/deno/src/std-node-crypto.ts b/deno/src/std-node-crypto.ts
new file mode 100644
index 0000000..873362f
--- /dev/null
+++ b/deno/src/std-node-crypto.ts
@@ -0,0 +1,122 @@
+import { Buffer } from "https://deno.land/std/node/buffer.ts";
+
+export default {createCipheriv, createDecipheriv}
+
+// export function createCipheriv(
+// algorithm: CipherCCMTypes,
+// key: CipherKey,
+// iv: BinaryLike | null,
+// options: CipherCCMOptions
+// ): CipherCCM
+// export function createCipheriv(
+// algorithm: CipherGCMTypes,
+// key: CipherKey,
+// iv: BinaryLike | null,
+// options?: CipherGCMOptions
+// ): CipherGCM
+export function createCipheriv(
+ algorithm: string,
+ key: CipherKey,
+ iv: BinaryLike | null,
+ options?: any//stream.TransformOptions
+): Cipher{
+ return new Cipher()
+}
+
+class Cipher /*extends stream.Transform */{
+ // private constructor()
+ update(data: BinaryLike): Buffer{
+ console.log('todo: crypto.Cipher.update')
+ return Buffer.from([])
+ }
+ // update(data: string, input_encoding: Utf8AsciiBinaryEncoding): Buffer
+ // update(
+ // data: NodeJS.ArrayBufferView,
+ // input_encoding: undefined,
+ // output_encoding: HexBase64BinaryEncoding
+ // ): string
+ // update(
+ // data: string,
+ // input_encoding: Utf8AsciiBinaryEncoding | undefined,
+ // output_encoding: HexBase64BinaryEncoding
+ // ): string
+ final(): Buffer {
+ console.log('todo: crypto.Cipher.update')
+ return Buffer.from([])
+ }
+ // final(output_encoding: BufferEncoding): string
+ // setAutoPadding(auto_padding?: boolean): this
+ getAuthTag(): Buffer {
+ console.log('todo: crypto.Cipher.update')
+ return Buffer.from([])
+ }
+ // setAAD(buffer: Buffer): this; // docs only say buffer
+}
+
+
+export function createDecipheriv(algorithm: string, key: CipherKey, iv: BinaryLike | null, options?:any/* stream.TransformOptions*/): Decipher{
+ return new Decipher()
+}
+
+
+ class Decipher /* extends stream.Transform*/ {
+ // private constructor();
+ update(data: BinaryLike): Buffer{
+ console.log('todo: crypto.Decipher.update')
+ return Buffer.from([])
+ }
+ // update(data: NodeJS.ArrayBufferView): Buffer;
+ // update(data: string, input_encoding: HexBase64BinaryEncoding): Buffer;
+ // update(data: NodeJS.ArrayBufferView, input_encoding: HexBase64BinaryEncoding | undefined, output_encoding: Utf8AsciiBinaryEncoding): string;
+ // update(data: string, input_encoding: HexBase64BinaryEncoding | undefined, output_encoding: Utf8AsciiBinaryEncoding): string;
+ final(): Buffer {
+ console.log('todo: crypto.Decipher.final')
+ return Buffer.from([])
+ }
+ // final(output_encoding: BufferEncoding): string;
+ // setAutoPadding(auto_padding?: boolean): this;
+ setAuthTag(tag: Buffer): this {
+ console.log('todo: crypto.Decipher.setAuthTag')
+ return this
+ }
+ // setAAD(buffer: NodeJS.ArrayBufferView): this;
+ }
+
+// Types
+
+ // type KeyObjectType = 'secret' | 'public' | 'private';
+
+ // interface KeyExportOptions {
+ // type: 'pkcs1' | 'spki' | 'pkcs8' | 'sec1';
+ // format: T;
+ // cipher?: string;
+ // passphrase?: string | Buffer;
+ // }
+
+ // class KeyObject {
+ // private constructor();
+ // asymmetricKeyType?: KeyType;
+ // *
+ // * For asymmetric keys, this property represents the size of the embedded key in
+ // * bytes. This property is `undefined` for symmetric keys.
+
+ // asymmetricKeySize?: number;
+ // export(options: KeyExportOptions<'pem'>): string | Buffer;
+ // export(options?: KeyExportOptions<'der'>): Buffer;
+ // symmetricKeySize?: number;
+ // type: KeyObjectType;
+ // }
+
+ type CipherCCMTypes = 'aes-128-ccm' | 'aes-192-ccm' | 'aes-256-ccm' | 'chacha20-poly1305';
+ type CipherGCMTypes = 'aes-128-gcm' | 'aes-192-gcm' | 'aes-256-gcm';
+
+ type BinaryLike = string | Uint8Array// | NodeJS.ArrayBufferView;
+
+ type CipherKey = BinaryLike// | KeyObject;
+
+ interface CipherCCMOptions /*extends stream.TransformOptions*/ {
+ authTagLength: number;
+ }
+ interface CipherGCMOptions /*extends stream.TransformOptions*/ {
+ authTagLength?: number;
+ }
\ No newline at end of file
diff --git a/deno/src/std-node-net.ts b/deno/src/std-node-net.ts
new file mode 100644
index 0000000..aa7b684
--- /dev/null
+++ b/deno/src/std-node-net.ts
@@ -0,0 +1,325 @@
+import { Buffer } from 'https://deno.land/std/node/buffer.ts'
+import { EventEmitter } from 'https://deno.land/std/node/events.ts'
+import { Duplex } from './std-node-stream.ts'
+
+export class Socket extends Duplex {
+ #connection?: Deno.Conn
+
+ constructor(options?: SocketConstructorOpts) {
+ super()
+ console.log('todo: net.Socket', options)
+ // setTimeout(() => {}, 3000)
+ }
+ // // Extended base methods
+ // write(buffer: Uint8Array | string, cb?: (err?: Error) => void): boolean;
+ // write(str: Uint8Array | string, encoding?: BufferEncoding, cb?: (err?: Error) => void): boolean;
+
+ // connect(port: number, host: string, connectionListener?: () => void): this;
+ // connect(port: number, connectionListener?: () => void): this;
+ // connect(path: string, connectionListener?: () => void): this;
+
+ connect(options: SocketConnectOpts, connectionListener?: () => void): this {
+ if (!('host' in options)) {
+ throw new Error('host missing')
+ }
+
+ console.log('todo: net.Socket.connect', options)
+
+ if (this.write !== Socket.prototype.write)
+ this.write = Socket.prototype.write
+
+ // if (this.destroyed) {
+ // this._handle = null;
+ // this._peername = null;
+ // this._sockname =s null;
+ // }
+ if (typeof connectionListener === 'function') {
+ this.once('connect', connectionListener)
+ }
+ this.connecting = true
+ this.writable = true
+
+ setTimeout(() =>
+ Deno.connect({
+ hostname: options?.host,
+ port: Number(options?.port),
+ }).then(async conn => {
+ this.#connection = conn
+ this.connecting = false
+ this.emit('connect')
+ this.emit('ready')
+
+ const i = setInterval(() => {
+ const ok = new Uint8Array(32)
+ conn.read(ok).then(len => {
+ console.log('read', ok)
+ })
+ // console.log('reading', ok)
+ }, 1000)
+
+ console.log('todo: net.connect.connected', conn.read(new Uint8Array(0)))
+ // await Promise.all([Deno.copy(conn, this), Deno.copy(this, conn)]);
+ })
+ )
+
+ return this
+ }
+
+ // setEncoding(encoding?: BufferEncoding): this;
+ // pause(): this;
+ // resume(): this;
+ // setTimeout(timeout: number, callback?: () => void): this;
+ // setNoDelay(noDelay?: boolean): this;
+ // setKeepAlive(enable?: boolean, initialDelay?: number): this;
+ // address(): AddressInfo | string;
+ // unref(): this;
+ // ref(): this;
+
+ // readonly bufferSize: number;
+ // readonly bytesRead: number;
+ // readonly bytesWritten: number;
+ /*readonly*/ connecting: boolean = false
+ // readonly destroyed: boolean;
+ // readonly localAddress: string;
+ // readonly localPort: number;
+ // readonly remoteAddress?: string;
+ // readonly remoteFamily?: string;
+ // readonly remotePort?: number;
+ /* override because readonly*/ writable: boolean = false
+
+ read(len: number): Uint8Array {
+ console.log('todo: net.Socket.read', len)
+ const buf = new Uint8Array(len)
+ this.#connection?.read(buf) || buf
+ return buf
+ // return super.read(len)
+ // return new Uint8Array(8)
+ }
+ // write(buffer: Uint8Array | string, cb?: (err?: Error) => void): boolean;
+ // write(str: Uint8Array | string, encoding?: BufferEncoding, cb?: (err?: Error) => void): boolean;
+ write(str: Uint8Array): boolean {
+ console.log(' ok: net.Socket.write', str)
+ this.#connection?.write(str)
+ return super.write(str)
+ }
+ end(): boolean {
+ console.log('todo: net.Socket.end')
+ return false
+ }
+ [Symbol.asyncIterator](): AsyncIterableIterator {
+ console.log('todo: net.Socket.asyncIterator')
+ return false as any
+ }
+}
+
+export interface AddressInfo {
+ address: string
+ family: string
+ port: number
+}
+
+export class Server extends EventEmitter {
+ #listener?: (socket: Socket) => void
+
+ constructor(connectionListener?: (socket: Socket) => void) {
+ super()
+ console.log('todo: net.Server')
+ this.#listener = connectionListener
+ }
+
+ unref() {
+ console.log('todo: net.Server.unref')
+ }
+
+ close() {
+ console.log('todo: net.Server.close')
+ }
+ address(): AddressInfo {
+ console.log('todo: net.Server.address')
+ return ({} as unknown) as AddressInfo
+ }
+
+ // listen(port?: number, hostname?: string, backlog?: number, listeningListener?: () => void): this;
+ // listen(port?: number, hostname?: string, listeningListener?: () => void): this;
+ // listen(port?: number, backlog?: number, listeningListener?: () => void): this;
+ // listen(port?: number, listeningListener?: () => void): this;
+ // listen(path: string, backlog?: number, listeningListener?: () => void): this;
+ // listen(path: string, listeningListener?: () => void): this;
+ listen(options?: ListenOptions, listeningListener?: () => void): this {
+ console.log('todo: net.Server.listen')
+ return this
+ }
+ // listen(handle: any, backlog?: number, listeningListener?: () => void): this;
+ // listen(handle: any, listeningListener?: () => void): this;
+ // close(callback?: (err?: Error) => void): this;
+ // address(): AddressInfo | string | null;
+ // getConnections(cb: (error: Error | null, count: number) => void): void;
+ // ref(): this;
+ // unref(): this;
+ // maxConnections: number;
+ // connections: number;
+ // listening: boolean;
+
+ // /**
+ // * events.EventEmitter
+ // * 1. close
+ // * 2. connection
+ // * 3. error
+ // * 4. listening
+ // */
+ // addListener(event: string, listener: (...args: any[]) => void): this;
+ // addListener(event: "close", listener: () => void): this;
+ // addListener(event: "connection", listener: (socket: Socket) => void): this;
+ // addListener(event: "error", listener: (err: Error) => void): this;
+ // addListener(event: "listening", listener: () => void): this;
+
+ // emit(event: string | symbol, ...args: any[]): boolean;
+ // emit(event: "close"): boolean;
+ // emit(event: "connection", socket: Socket): boolean;
+ // emit(event: "error", err: Error): boolean;
+ // emit(event: "listening"): boolean;
+
+ // on(event: string, listener: (...args: any[]) => void): this;
+ // on(event: "close", listener: () => void): this;
+ // on(event: "connection", listener: (socket: Socket) => void): this;
+ // on(event: "error", listener: (err: Error) => void): this;
+ // on(event: "listening", listener: () => void): this;
+
+ // once(event: string, listener: (...args: any[]) => void): this;
+ // once(event: "close", listener: () => void): this;
+ // once(event: "connection", listener: (socket: Socket) => void): this;
+ // once(event: "error", listener: (err: Error) => void): this;
+ // once(event: "listening", listener: () => void): this;
+
+ // prependListener(event: string, listener: (...args: any[]) => void): this;
+ // prependListener(event: "close", listener: () => void): this;
+ // prependListener(event: "connection", listener: (socket: Socket) => void): this;
+ // prependListener(event: "error", listener: (err: Error) => void): this;
+ // prependListener(event: "listening", listener: () => void): this;
+
+ // prependOnceListener(event: string, listener: (...args: any[]) => void): this;
+ // prependOnceListener(event: "close", listener: () => void): this;
+ // prependOnceListener(event: "connection", listener: (socket: Socket) => void): this;
+ // prependOnceListener(event: "error", listener: (err: Error) => void): this;
+ // prependOnceListener(event: "listening", listener: () => void): this;
+}
+
+// Types
+
+export type LookupFunction = (
+ hostname: string,
+ options: any /*dns.LookupOneOptions*/,
+ callback: (
+ err: any /*NodeJS.ErrnoException | null*/,
+ address: string,
+ family: number
+ ) => void
+) => void
+
+export interface AddressInfo {
+ address: string
+ family: string
+ port: number
+}
+
+export interface SocketConstructorOpts {
+ fd?: number
+ allowHalfOpen?: boolean
+ readable?: boolean
+ writable?: boolean
+}
+
+export interface OnReadOpts {
+ buffer: Uint8Array | (() => Uint8Array)
+ /**
+ * This function is called for every chunk of incoming data.
+ * Two arguments are passed to it: the number of bytes written to buffer and a reference to buffer.
+ * Return false from this function to implicitly pause() the socket.
+ */
+ callback(bytesWritten: number, buf: Uint8Array): boolean
+}
+
+export interface ConnectOpts {
+ /**
+ * If specified, incoming data is stored in a single buffer and passed to the supplied callback when data arrives on the socket.
+ * Note: this will cause the streaming functionality to not provide any data, however events like 'error', 'end', and 'close' will
+ * still be emitted as normal and methods like pause() and resume() will also behave as expected.
+ */
+ onread?: OnReadOpts
+}
+
+export interface TcpSocketConnectOpts extends ConnectOpts {
+ port: number
+ host?: string
+ localAddress?: string
+ localPort?: number
+ hints?: number
+ family?: number
+ lookup?: LookupFunction
+}
+
+export interface IpcSocketConnectOpts extends ConnectOpts {
+ path: string
+}
+
+export type SocketConnectOpts = TcpSocketConnectOpts | IpcSocketConnectOpts
+
+export interface ListenOptions {
+ port?: number
+ host?: string
+ backlog?: number
+ path?: string
+ exclusive?: boolean
+ readableAll?: boolean
+ writableAll?: boolean
+ /**
+ * @default false
+ */
+ ipv6Only?: boolean
+}
+
+export interface TcpNetConnectOpts
+ extends TcpSocketConnectOpts,
+ SocketConstructorOpts {
+ timeout?: number
+}
+
+export interface IpcNetConnectOpts
+ extends IpcSocketConnectOpts,
+ SocketConstructorOpts {
+ timeout?: number
+}
+
+export type NetConnectOpts = TcpNetConnectOpts | IpcNetConnectOpts
+
+export function createServer(
+ connectionListener?: (socket: Socket) => void
+): Server {
+ console.log('todo: net.createServer')
+ return new Server(console.log)
+}
+// export function createServer(options?: { allowHalfOpen?: boolean, pauseOnConnect?: boolean }, connectionListener?: (socket: Socket) => void): Server;
+export function connect(
+ options: NetConnectOpts,
+ connectionListener?: () => void
+): Socket {
+ const socket = new Socket(options)
+
+ if (options.timeout) {
+ // socket.setTimeout(options.timeout);
+ }
+
+ return socket.connect(options)
+}
+// export function connect(port: number, host?: string, connectionListener?: () => void): Socket;
+// export function connect(path: string, connectionListener?: () => void): Socket;
+// export function createConnection(options: NetConnectOpts, connectionListener?: () => void): Socket;
+// export function createConnection(port: number, host?: string, connectionListener?: () => void): Socket;
+// export function createConnection(path: string, connectionListener?: () => void): Socket;
+// export function isIP(input: string): number;
+export function isIPv4(input: string): boolean {
+ return true
+}
+export function isIPv6(input: string): boolean {
+ return false
+}
diff --git a/deno/src/std-node-stream.ts b/deno/src/std-node-stream.ts
new file mode 100644
index 0000000..4177b93
--- /dev/null
+++ b/deno/src/std-node-stream.ts
@@ -0,0 +1,442 @@
+import * as events from "https://deno.land/std/node/events.ts"
+
+export class Stream extends events.EventEmitter {
+ constructor(opts?: ReadableOptions) {
+ super()
+ console.log('todo: stream.Stream')
+ }
+ pipe(destination: T, options?: { end?: boolean }): T {
+ return destination
+ }
+ on(event:any, listener: any){
+ console.log('todo: stream.Stream.on', event)
+ return super.on(event, listener)
+ }
+ emit(event:any, data?: any){
+ console.log('todo: stream.Stream.emit', event, data)
+ return super.emit(event, data)
+ }
+ once(event:any, listener: any){
+ // console.log('todo: stream.Stream.once', event)
+ return super.once(event, listener)
+ }
+}
+
+
+interface ReadableOptions {
+ highWaterMark?: number;
+ encoding?: BufferEncoding;
+ objectMode?: boolean;
+ read?(this: Readable, size: number): void;
+ destroy?(this: Readable, error: Error | null, callback: (error: Error | null) => void): void;
+ autoDestroy?: boolean;
+}
+
+export class Readable extends Stream {
+ #needReadable = false;
+ #emittedReadable = false
+
+ /**
+ * A utility method for creating Readable Streams out of iterators.
+ */
+ static from(
+ iterable: Iterable | AsyncIterable,
+ options?: ReadableOptions
+ ): Readable{
+ console.log('todo: stream.Readable')
+ return new Readable()}
+
+ // readable: boolean
+ // readonly readableEncoding: BufferEncoding | null
+ // readonly readableEnded: boolean
+ // readonly readableHighWaterMark: number
+ // readonly readableLength: number
+ // readonly readableObjectMode: boolean
+ destroyed: boolean=false
+ // constructor(opts?: ReadableOptions)
+ _read(size: number): void{ console.log('TODO _read') }
+ read(size?: number): any{
+ console.log('todo: stream.Readable.read', size)
+
+ }
+ // setEncoding(encoding: BufferEncoding): this
+ // pause(): this
+ // resume(): this
+ // isPaused(): boolean
+ // unpipe(destination?: NodeJS.WritableStream): this
+ // unshift(chunk: any, encoding?: BufferEncoding): void
+ // wrap(oldStream: NodeJS.ReadableStream): this
+ push(chunk: any, encoding?: BufferEncoding): boolean{
+ console.log('todo: stream.Readable.push')
+ return true
+ }
+ _destroy(error: Error | null, callback: (error?: Error | null) => void): void{ console.log('TODO _destroy') }
+ destroy(error?: Error): void{ console.log('TODO destroy') }
+
+
+ on(event:any, listener: any){
+ console.log('todo: stream.Readable.on', event)
+ const ok = super.on(event, listener)
+ if (event ==='readable'&&!this.#emittedReadable) {
+ this.#emittedReadable = true
+ super.emit('readable', this)
+ }
+ return ok
+ }
+ emit(event:any, data?: any){
+ console.log('todo: stream.Readable.emit', event, data)
+ return super.emit(event, data)
+ }
+ once(event:any, listener: any){
+ // console.log('todo: stream.Readable.once', event)
+ return super.once(event, listener)
+ }
+
+ /**
+ * Event emitter
+ * The defined events on documents including:
+ * 1. close
+ * 2. data
+ * 3. end
+ * 4. error
+ * 5. pause
+ * 6. readable
+ * 7. resume
+ */
+ // addListener(event: 'close', listener: () => void): this
+ // addListener(event: 'data', listener: (chunk: any) => void): this
+ // addListener(event: 'end', listener: () => void): this
+ // addListener(event: 'error', listener: (err: Error) => void): this
+ // addListener(event: 'pause', listener: () => void): this
+ // addListener(event: 'readable', listener: () => void): this
+ // addListener(event: 'resume', listener: () => void): this
+ // addListener(event: string | symbol, listener: (...args: any[]) => void): this {
+ // console.log('todo: stream.Readable.addListener', event)
+ // if (!this.#listeners[event]) {
+ // this.#listeners[event] =[]
+ // }
+ // this.#listeners[event].push(listener)
+ // super.addListener(event, listener)
+ // return this
+ // }
+
+ // emit(event: 'close'): boolean
+ // emit(event: 'data', chunk: any): boolean
+ // emit(event: 'end'): boolean
+ // emit(event: 'error', err: Error): boolean
+ // emit(event: 'pause'): boolean
+ // emit(event: 'readable'): boolean
+ // emit(event: 'resume'): boolean
+ // emit(event: string | symbol, ...args: any[]): boolean {
+ // console.log('todo: stream.Readable.emit', event)
+ // return true
+
+ // }
+
+ // on(event: 'close', listener: () => void): this
+ // on(event: 'data', listener: (chunk: any) => void): this
+ // on(event: 'end', listener: () => void): this
+ // on(event: 'error', listener: (err: Error) => void): this
+ // on(event: 'pause', listener: () => void): this
+ // on(event: 'readable', listener: () => void): this
+ // on(event: 'resume', listener: () => void): this
+ // on(event: string | symbol, listener: (...args: any[]) => void): this {
+ // console.log('todo: stream.Readable.on', event)
+ // if (!this.#listeners[event]) {
+ // this.#listeners[event] =[]
+ // }
+ // this.#listeners[event].push(listener)
+ // return this
+
+ // }
+
+ // once(event: 'close', listener: () => void): this
+ // once(event: 'data', listener: (chunk: any) => void): this
+ // once(event: 'end', listener: () => void): this
+ // once(event: 'error', listener: (err: Error) => void): this
+ // once(event: 'pause', listener: () => void): this
+ // once(event: 'readable', listener: () => void): this
+ // once(event: 'resume', listener: () => void): this
+ // once(event: string | symbol, listener: (...args: any[]) => void): this {
+ // if (!this.#listeners[event]) {
+ // this.#listeners[event] =[]
+ // }
+ // this.#listeners[event].push(listener)
+ // console.log('todo: stream.Readable.once', event)
+ // return this
+
+ // }
+
+ // prependListener(event: 'close', listener: () => void): this
+ // prependListener(event: 'data', listener: (chunk: any) => void): this
+ // prependListener(event: 'end', listener: () => void): this
+ // prependListener(event: 'error', listener: (err: Error) => void): this
+ // prependListener(event: 'pause', listener: () => void): this
+ // prependListener(event: 'readable', listener: () => void): this
+ // prependListener(event: 'resume', listener: () => void): this
+ // prependListener(
+ // event: string | symbol,
+ // listener: (...args: any[]) => void
+ // ): this {
+ // console.log('todo: stream.Readable.prependListener')
+ // return this
+
+ // }
+
+ // prependOnceListener(event: 'close', listener: () => void): this
+ // prependOnceListener(event: 'data', listener: (chunk: any) => void): this
+ // prependOnceListener(event: 'end', listener: () => void): this
+ // prependOnceListener(event: 'error', listener: (err: Error) => void): this
+ // prependOnceListener(event: 'pause', listener: () => void): this
+ // prependOnceListener(event: 'readable', listener: () => void): this
+ // prependOnceListener(event: 'resume', listener: () => void): this
+ // prependOnceListener(
+ // event: string | symbol,
+ // listener: (...args: any[]) => void
+ // ): this {
+ // console.log('todo: stream.Readable.prependOnceListener')
+ // return this
+
+ // }
+
+ // removeListener(event: 'close', listener: () => void): this
+ // removeListener(event: 'data', listener: (chunk: any) => void): this
+ // removeListener(event: 'end', listener: () => void): this
+ // removeListener(event: 'error', listener: (err: Error) => void): this
+ // removeListener(event: 'pause', listener: () => void): this
+ // removeListener(event: 'readable', listener: () => void): this
+ // removeListener(event: 'resume', listener: () => void): this
+ // removeListener(
+ // event: string | symbol,
+ // listener: (...args: any[]) => void
+ // ): this {
+ // console.log('todo: stream.Readable.removeListener')
+ // return this
+ // }
+
+ [Symbol.asyncIterator](): AsyncIterableIterator {
+ console.log('todo: stream.Readable.asyncIterator')
+ return false as any
+ }
+}
+
+interface WritableOptions {
+ highWaterMark?: number;
+ decodeStrings?: boolean;
+ defaultEncoding?: BufferEncoding;
+ objectMode?: boolean;
+ emitClose?: boolean;
+ write?(this: Writable, chunk: any, encoding: BufferEncoding, callback: (error?: Error | null) => void): void;
+ writev?(this: Writable, chunks: Array<{ chunk: any, encoding: BufferEncoding }>, callback: (error?: Error | null) => void): void;
+ destroy?(this: Writable, error: Error | null, callback: (error: Error | null) => void): void;
+ final?(this: Writable, callback: (error?: Error | null) => void): void;
+ autoDestroy?: boolean;
+}
+
+export class Writable extends Stream /* implements NodeJS.WritableStream */ {
+ readonly writable: boolean = true;
+ readonly writableEnded: boolean = false;
+ readonly writableFinished: boolean = false;
+ // readonly writableHighWaterMark: number;
+ // readonly writableLength: number;
+ // readonly writableObjectMode: boolean;
+ // readonly writableCorked: number;
+ destroyed: boolean = false
+ // constructor(opts?: WritableOptions);
+ _write(chunk: any, encoding: BufferEncoding, callback: (error?: Error | null) => void): void{ console.log('TODO _write') }
+ _writev?(chunks: Array<{ chunk: any, encoding: BufferEncoding }>, callback: (error?: Error | null) => void): void{ console.log('TODO _writev') }
+ _destroy(error: Error | null, callback: (error?: Error | null) => void): void{ console.log('TODO _destroy') }
+ _final(callback: (error?: Error | null) => void): void{ console.log('TODO _final') }
+ write(chunk: any, cb?: (error: Error | null | undefined) => void): boolean{
+ console.log(' ok: stream.Writable.write')
+ return super.push(chunk, cb)
+ }
+ // write(chunk: any, encoding: BufferEncoding, cb?: (error: Error | null | undefined) => void): boolean
+ // setDefaultEncoding(encoding: BufferEncoding): this;
+ end(cb?: () => void): void{ console.log('TODO end') }
+ // end(chunk: any, cb?: () => void): void
+ // end(chunk: any, encoding: BufferEncoding, cb?: () => void): void{ console.log('TODO //') }
+ cork(): void{ console.log('TODO cork') }
+ uncork(): void{ console.log('TODO uncork') }
+ destroy(error?: Error): void{
+ console.log('todo: stream.Writable.destroy')
+
+ }
+
+ /**
+ * Event emitter
+ * The defined events on documents including:
+ * 1. close
+ * 2. drain
+ * 3. error
+ * 4. finish
+ * 5. pipe
+ * 6. unpipe
+ */
+ // addListener(event: "close", listener: () => void): this;
+ // addListener(event: "drain", listener: () => void): this;
+ // addListener(event: "error", listener: (err: Error) => void): this;
+ // addListener(event: "finish", listener: () => void): this;
+ // addListener(event: "pipe", listener: (src: Readable) => void): this;
+ // addListener(event: "unpipe", listener: (src: Readable) => void): this;
+ // addListener(event: string | symbol, listener: (...args: any[]) => void): this {
+ // console.log('todo: stream.Writable.addListener')
+ // return this
+ // }
+
+ // emit(event: "close"): boolean;
+ // emit(event: "drain"): boolean;
+ // emit(event: "error", err: Error): boolean;
+ // emit(event: "finish"): boolean;
+ // emit(event: "pipe", src: Readable): boolean;
+ // emit(event: "unpipe", src: Readable): boolean;
+ // emit(event: string | symbol, ...args: any[]): boolean {
+ // console.log('todo: stream.Writable.emit')
+ // return true
+ // }
+
+ // on(event: "close", listener: () => void): this;
+ // on(event: "drain", listener: () => void): this;
+ // on(event: "error", listener: (err: Error) => void): this;
+ // on(event: "finish", listener: () => void): this;
+ // on(event: "pipe", listener: (src: Readable) => void): this;
+ // on(event: "unpipe", listener: (src: Readable) => void): this;
+ // on(event: string | symbol, listener: (...args: any[]) => void): this {
+ // console.log('todo: stream.Writable.on')
+ // return this
+ // }
+
+ // once(event: "close", listener: () => void): this;
+ // once(event: "drain", listener: () => void): this;
+ // once(event: "error", listener: (err: Error) => void): this;
+ // once(event: "finish", listener: () => void): this;
+ // once(event: "pipe", listener: (src: Readable) => void): this;
+ // once(event: "unpipe", listener: (src: Readable) => void): this;
+ // once(event: string | symbol, listener: (...args: any[]) => void): this {
+ // console.log('todo: stream.Writable.once')
+ // return this
+ // }
+
+ // prependListener(event: "close", listener: () => void): this;
+ // prependListener(event: "drain", listener: () => void): this;
+ // prependListener(event: "error", listener: (err: Error) => void): this;
+ // prependListener(event: "finish", listener: () => void): this;
+ // prependListener(event: "pipe", listener: (src: Readable) => void): this;
+ // prependListener(event: "unpipe", listener: (src: Readable) => void): this;
+ // prependListener(event: string | symbol, listener: (...args: any[]) => void): this {
+ // console.log('todo: stream.Writable.prependListener')
+ // return this
+ // }
+
+ // prependOnceListener(event: "close", listener: () => void): this;
+ // prependOnceListener(event: "drain", listener: () => void): this;
+ // prependOnceListener(event: "error", listener: (err: Error) => void): this;
+ // prependOnceListener(event: "finish", listener: () => void): this;
+ // prependOnceListener(event: "pipe", listener: (src: Readable) => void): this;
+ // prependOnceListener(event: "unpipe", listener: (src: Readable) => void): this;
+ // prependOnceListener(event: string | symbol, listener: (...args: any[]) => void): this {
+ // console.log('todo: stream.Writable.prependOnceListener')
+ // return this
+ // }
+
+ // removeListener(event: "close", listener: () => void): this;
+ // removeListener(event: "drain", listener: () => void): this;
+ // removeListener(event: "error", listener: (err: Error) => void): this;
+ // removeListener(event: "finish", listener: () => void): this;
+ // removeListener(event: "pipe", listener: (src: Readable) => void): this;
+ // removeListener(event: "unpipe", listener: (src: Readable) => void): this;
+ // removeListener(event: string | symbol, listener: (...args: any[]) => void): this {
+ // console.log('todo: stream.Writable.removeListener')
+ // return this
+ // }
+}
+
+
+interface SocketConstructorOpts {
+ fd?: number
+ allowHalfOpen?: boolean
+ readable?: boolean
+ writable?: boolean
+}
+
+type BufferEncoding = string
+
+export function finished(dup: Duplex, options: any) {}
+
+
+// @ts-ignore
+interface DuplexOptions extends ReadableOptions, WritableOptions {
+ // allowHalfOpen?: boolean;
+ // readableObjectMode?: boolean;
+ // writableObjectMode?: boolean;
+ // readableHighWaterMark?: number;
+ // writableHighWaterMark?: number;
+ // writableCorked?: number;
+ read?(this: Duplex, size: number): void;
+ write?(this: Duplex, chunk: any, encoding: BufferEncoding, callback: (error?: Error | null) => void): void;
+ // writev?(this: Duplex, chunks: Array<{ chunk: any, encoding: BufferEncoding }>, callback: (error?: Error | null) => void): void;
+ // final?(this: Duplex, callback: (error?: Error | null) => void): void;
+ destroy?(this: Duplex, error: Error | null, callback: (error: Error | null) => void): void;
+}
+
+// Note: Duplex extends both Readable and Writable.
+// @ts-ignore
+export class Duplex extends Readable implements Writable {
+ readonly writable: boolean = true;
+ readonly writableEnded: boolean = false;
+ readonly writableFinished: boolean = false;
+ // readonly writableHighWaterMark: number = 1;
+ // readonly writableLength: number = 1;
+ // readonly writableObjectMode: boolean = false;
+ // readonly writableCorked: number = 1;
+ constructor(opts?: DuplexOptions) {
+ super()
+ }
+ _write(chunk: any, encoding: BufferEncoding, callback: (error?: Error | null) => void): void {
+ console.log('todo: stream.duplex._write')
+ }
+ _writev?(chunks: Array<{ chunk: any, encoding: BufferEncoding }>, callback: (error?: Error | null) => void): void {
+ console.log('todo: stream.duplex._writev')
+ }
+ _destroy(error: Error | null, callback: (error: Error | null) => void): void {
+ console.log('todo: stream.duplex._destroy')
+ }
+ _final(callback: (error?: Error | null) => void): void {
+ console.log('todo: stream.duplex._final')
+ }
+ write(chunk: any, cb?: (error: Error | null | undefined) => void): boolean {
+ console.log('todo: stream.duplex.write')
+ return true
+ }
+ setDefaultEncoding(encoding: BufferEncoding): this {
+ console.log('todo: stream.duplex.setDefaultEncoding')
+ return this
+ }
+ end(cb?: () => void): void {
+ console.log('todo: stream.duplex.end')
+
+ }
+ cork(): void {}
+ uncork(): void {}
+
+
+ // Wirtable?
+ // [Symbol.asyncIterator]() {
+ // return {
+ // next() {
+ // return new Promise<{ value: any }>(res => {})
+ // },
+ // }
+ // }
+}
+applyMixins (Duplex, [Writable]);
+
+function applyMixins(derivedCtor: any, baseCtors: any[]) {
+ baseCtors.forEach(baseCtor => {
+ Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
+ if (name !== 'constructor') {
+ derivedCtor.prototype[name] = baseCtor.prototype[name];
+ }
+ });
+ });
+}
\ No newline at end of file
diff --git a/deno/src/stream.ts b/deno/src/stream.ts
new file mode 100644
index 0000000..7f95fe8
--- /dev/null
+++ b/deno/src/stream.ts
@@ -0,0 +1,204 @@
+import { Buffer } from 'https://deno.land/std/node/buffer.ts'
+import { Readable, Writable } from "./std-node-stream.ts"
+import * as events from "https://deno.land/std/node/events.ts";
+import { DataPacket, ServiceRequestPacket } from "./packet.ts";
+
+export async function drain(writable: Writable) {
+ if (writable.destroyed) throw new Error(`premature close`);
+
+ await Promise.race([
+ events.once(writable, "drain"),
+ events.once(writable, "close").then(() => {
+ throw new Error(`premature close`);
+ }),
+ ]);
+}
+
+export async function* lengthPrefixed(stream: AsyncIterable) {
+ // @ts-ignore
+ let buf: Buffer = Buffer.of();
+ let size: number | undefined;
+
+ for await (const data of stream) {
+ buf = Buffer.concat([buf, data]);
+
+ while (true) {
+ if (!size) {
+ if (buf.byteLength < 4) break;
+ size = buf.readUInt32BE(0);
+ buf = buf.slice(4);
+ }
+
+ if (buf.byteLength < size) break;
+
+ const frame = buf.slice(0, size);
+ buf = buf.slice(size);
+ size = undefined;
+
+ yield frame;
+ }
+ }
+}
+
+export function prefixLength(src: string | ArrayBufferLike): Buffer {
+ // @ts-ignore
+ const data = Buffer.isBuffer(src) ? src : Buffer.from(src);
+
+ const header = Buffer.alloc(4);
+ header.writeUInt32BE(data.byteLength);
+ return Buffer.concat([header, data]);
+}
+
+export class RPC {
+ pending: events.EventEmitter = new events.EventEmitter();
+ initial: number;
+ counter: number;
+
+ constructor(client: boolean) {
+ this.counter = this.initial = client ? 1 : 2;
+ }
+
+ next(): number {
+ const seq = this.counter;
+ if ((this.counter += 2) === 0) {
+ this.counter = this.initial;
+ }
+ return seq;
+ }
+
+ message(seq: number, src: string | ArrayBufferLike): Buffer {
+ // @ts-ignore
+ const buf = Buffer.isBuffer(src) ? src : Buffer.from(src);
+ const header = Buffer.alloc(4);
+ header.writeUInt32BE(seq);
+ return Buffer.concat([header, buf]);
+ }
+
+ request(src: string | ArrayBufferLike): [Buffer, Promise] {
+ const seq = this.next();
+ const buf = this.message(seq, src);
+ return [buf, this.wait(seq)];
+ }
+
+ async wait(seq: number): Promise {
+ return (<[Buffer]>await events.once(this.pending, `${seq}`))[0];
+ }
+
+ async *parse(stream: AsyncIterable) {
+ for await (let frame of stream) {
+ if (frame.byteLength < 4)
+ throw new Error(
+ `Frame must be prefixed with an unsigned 32-bit sequence number.`
+ );
+
+ const seq = frame.readUInt32BE();
+ frame = frame.slice(4);
+
+ if (seq !== 0 && seq % 2 === this.initial % 2) {
+ this.pending.emit(`${seq}`, frame);
+ continue;
+ }
+
+ yield { seq, frame };
+ }
+ }
+}
+
+export const STREAM_CHUNK_SIZE = 2048;
+
+export class Stream extends events.EventEmitter {
+ id: number;
+ body: Readable;
+ headers?: { [key: string]: string };
+
+ constructor(id: number) {
+ super();
+
+ this.id = id;
+
+ this.body = new Readable();
+ this.body._read = () => {
+ return;
+ };
+ }
+}
+
+export class Streams {
+ active = new Map();
+ initial: number;
+ counter: number;
+
+ constructor(client: boolean) {
+ this.counter = this.initial = client ? 0 : 1;
+ }
+
+ register(id?: number): Stream {
+ if (id === undefined) {
+ id = this.counter;
+ if ((this.counter += 2) === 0) {
+ this.counter = this.initial;
+ }
+ }
+
+ if (this.active.has(id)) {
+ throw new Error(
+ `Attempted to register stream with ID ${id} which already exists.`
+ );
+ }
+
+ const stream = new Stream(id);
+ this.active.set(id, stream);
+
+ return stream;
+ }
+
+ get(id: number): Stream | undefined {
+ return this.active.get(id);
+ }
+
+ push(
+ stream: Stream,
+ services: string[],
+ headers: { [key: string]: string }
+ ): [Buffer, Promise] {
+ return [
+ new ServiceRequestPacket(stream.id, services, headers).encode(),
+ this.wait(stream),
+ ];
+ }
+
+ pull(stream: Stream, handled: boolean, headers: { [key: string]: string }) {
+ stream.headers = headers;
+ // TODO: fix events.EventEmitter type
+ // @ts-ignore
+ stream.emit("ready", handled);
+ }
+
+ recv(stream: Stream, data: Buffer) {
+ if (data.byteLength === 0) {
+ this.active.delete(stream.id);
+ stream.body.push(null);
+ } else {
+ stream.body.push(data);
+ }
+ }
+
+ async *encoded(id: number, body: AsyncIterable) {
+ let buf: Buffer = Buffer.from([]);
+ for await (const chunk of body) {
+ buf = Buffer.concat([buf, Buffer.from(chunk)]);
+ while (buf.byteLength >= STREAM_CHUNK_SIZE) {
+ yield new DataPacket(id, buf.slice(0, STREAM_CHUNK_SIZE)).encode();
+ buf = buf.slice(STREAM_CHUNK_SIZE);
+ }
+ }
+ if (buf.byteLength > 0) {
+ yield new DataPacket(id, buf).encode();
+ }
+ yield new DataPacket(id, Buffer.from([])).encode();
+ }
+
+ async wait(stream: events.EventEmitter): Promise {
+ return (<[Boolean]>await events.once(stream, "ready"))[0];
+ }
+}
diff --git a/deno/tests/globals.d.spec.ts b/deno/tests/globals.d.spec.ts
new file mode 100644
index 0000000..5b3ab1b
--- /dev/null
+++ b/deno/tests/globals.d.spec.ts
@@ -0,0 +1,5 @@
+declare module Chai {
+ interface Assertion {
+ equalBytes(expected: ArrayBufferLike): Chai.Equal;
+ }
+}
diff --git a/deno/tests/sock.spec.ts b/deno/tests/sock.spec.ts
new file mode 100644
index 0000000..9ea30d3
--- /dev/null
+++ b/deno/tests/sock.spec.ts
@@ -0,0 +1,257 @@
+import "mocha";
+import * as net from "net";
+import * as events from "events";
+import { Node } from "../src";
+import chai, { expect } from "chai";
+import { Readable } from "stream";
+import { clientHandshake, serverHandshake, Session } from "../src/session";
+import * as nacl from "tweetnacl";
+import { ID } from "../src/kademlia";
+import { lengthPrefixed, prefixLength, RPC } from "../src/stream";
+import { Context } from "../src/context";
+import { getAvailableAddress } from "../src/net";
+
+chai.use(require("chai-bytes"));
+
+const createEndToEnd = async (): Promise<
+ [net.Server, net.Socket, net.Socket]
+> => {
+ const server = net.createServer();
+ server.listen();
+
+ await events.once(server, "listening");
+
+ const info = (server.address())!;
+ const client = net.createConnection(info.port, info.address);
+
+ const [[conn]] = <[[net.Socket], any[]]>(
+ await Promise.all([
+ events.once(server, "connection"),
+ events.once(client, "connect"),
+ ])
+ );
+
+ return [server, client, conn];
+};
+
+describe("length prefix", function () {
+ it("should work end-to-end", async () => {
+ const expected = [...Array(100)].map(() => Math.random().toString(16));
+
+ const [server, client, conn] = await createEndToEnd();
+
+ setImmediate(async () => {
+ for (const data of expected) {
+ expect(client.write(prefixLength(data))).to.equal(true);
+ }
+ client.end();
+ await events.once(client, "close");
+ });
+
+ const stream = lengthPrefixed(conn);
+ for (const data of expected) {
+ expect((await stream.next()).value).to.equalBytes(Buffer.from(data));
+ }
+ await events.once(conn, "close");
+
+ server.close();
+ await events.once(server, "close");
+ });
+});
+
+const endToEndHandshake = async (
+ client: net.Socket,
+ conn: net.Socket
+): Promise<[Uint8Array, Uint8Array]> => {
+ return await Promise.all([clientHandshake(client), serverHandshake(conn)]);
+};
+
+describe("session", function () {
+ it("should work end-to-end", async () => {
+ const expected = [...Array(100)].map(() => Math.random().toString(16));
+
+ const [server, client, conn] = await createEndToEnd();
+ const [clientSecret, serverSecret] = await endToEndHandshake(client, conn);
+
+ expect(clientSecret).to.equalBytes(serverSecret);
+
+ setImmediate(async () => {
+ const session = new Session(clientSecret);
+ const stream = session.decrypted(lengthPrefixed(client));
+
+ for (const data of expected) {
+ client.write(prefixLength(session.encrypt(data)));
+ }
+
+ for (const data of expected) {
+ expect((await stream.next()).value).to.equalBytes(Buffer.from(data));
+ }
+
+ client.end();
+ await events.once(client, "close");
+ });
+
+ const session = new Session(serverSecret);
+ const stream = session.decrypted(lengthPrefixed(conn));
+
+ for (const data of expected) {
+ expect((await stream.next()).value).to.equalBytes(Buffer.from(data));
+ }
+
+ for (const data of expected) {
+ conn.write(prefixLength(session.encrypt(data)));
+ }
+
+ await events.once(conn, "close");
+
+ server.close();
+ await events.once(server, "close");
+ });
+});
+
+describe("encrypted rpc", function () {
+ it("should work end-to-end", async () => {
+ const expected = [...Array(100)].map(() => Math.random().toString(16));
+
+ const [server, client, conn] = await createEndToEnd();
+ const [clientSecret, serverSecret] = await endToEndHandshake(client, conn);
+
+ expect(clientSecret).to.equalBytes(serverSecret);
+
+ setImmediate(async () => {
+ const rpc = new RPC(true);
+ const session = new Session(clientSecret);
+
+ const stream = rpc.parse(session.decrypted(lengthPrefixed(client)));
+
+ setImmediate(async () => {
+ while (true) {
+ const item = await stream.next();
+ if (item.done) {
+ break;
+ }
+
+ const { seq, frame } = item.value;
+
+ client.write(
+ prefixLength(
+ session.encrypt(
+ rpc.message(
+ seq,
+ Buffer.concat([Buffer.from("FROM CLIENT: "), frame])
+ )
+ )
+ )
+ );
+ }
+ });
+
+ for (const data of expected) {
+ const [req, res] = rpc.request(data);
+ client.write(prefixLength(session.encrypt(req)));
+ expect(await res).to.equalBytes(Buffer.from("FROM SERVER: " + data));
+ }
+
+ client.end();
+ await events.once(client, "close");
+ });
+
+ const rpc = new RPC(false);
+ const session = new Session(serverSecret);
+
+ const stream = rpc.parse(session.decrypted(lengthPrefixed(conn)));
+
+ setImmediate(async () => {
+ while (true) {
+ const item = await stream.next();
+ if (item.done) {
+ break;
+ }
+
+ const { seq, frame } = item.value;
+
+ conn.write(
+ prefixLength(
+ session.encrypt(
+ rpc.message(
+ seq,
+ Buffer.concat([Buffer.from("FROM SERVER: "), frame])
+ )
+ )
+ )
+ );
+ }
+ });
+
+ for (const data of expected) {
+ const [req, res] = rpc.request(data);
+ conn.write(prefixLength(session.encrypt(req)));
+ expect(await res).to.equalBytes(Buffer.from("FROM CLIENT: " + data));
+ }
+
+ await events.once(conn, "close");
+
+ server.close();
+ await events.once(server, "close");
+ });
+});
+
+describe("node", function () {
+ it("should work end-to-end", async () => {
+ const aliceAddr = await getAvailableAddress();
+ const bobAddr = await getAvailableAddress();
+
+ const alice = new Node();
+ const bob = new Node();
+
+ alice.keys = nacl.sign.keyPair();
+ bob.keys = nacl.sign.keyPair();
+
+ alice.id = new ID(alice.keys.publicKey, aliceAddr.host, aliceAddr.port);
+ bob.id = new ID(bob.keys.publicKey, bobAddr.host, bobAddr.port);
+
+ alice.handlers["hello_world"] = async (ctx: Context) => {
+ expect(ctx.id.publicKey).to.equalBytes(bob.id!.publicKey!);
+ expect(await ctx.body()).to.equalBytes(Buffer.from("Bob says hi!"));
+ ctx.send("Alice says hi!");
+ };
+
+ bob.handlers["hello_world"] = async (ctx: Context) => {
+ expect(ctx.id.publicKey).to.equalBytes(alice.id!.publicKey!);
+ expect(await ctx.body()).to.equalBytes(Buffer.from("Alice says hi!"));
+ ctx.send("Bob says hi!");
+ };
+
+ await bob.listen({ port: bobAddr.port });
+ await alice.connect({ host: bobAddr.host.toString(), port: bobAddr.port });
+
+ const aliceToBob = async () => {
+ for (let i = 0; i < 10; i++) {
+ const res = await alice.push(
+ ["hello_world"],
+ {},
+ Readable.from("Alice says hi!")
+ );
+ for await (const chunk of res.body) {
+ expect(chunk).to.equalBytes(Buffer.from("Bob says hi!"));
+ }
+ }
+ };
+
+ const bobToAlice = async () => {
+ for (let i = 0; i < 10; i++) {
+ const res = await bob.push(
+ ["hello_world"],
+ {},
+ Readable.from("Bob says hi!")
+ );
+ for await (const chunk of res.body) {
+ expect(chunk).to.equalBytes(Buffer.from("Alice says hi!"));
+ }
+ }
+ };
+
+ await Promise.all([aliceToBob(), bobToAlice()]);
+ await Promise.all([alice.shutdown(), bob.shutdown()]);
+ });
+});
diff --git a/deno/tests/tsconfig.json b/deno/tests/tsconfig.json
new file mode 100644
index 0000000..4475e5a
--- /dev/null
+++ b/deno/tests/tsconfig.json
@@ -0,0 +1,76 @@
+{
+ "compilerOptions": {
+ /* Visit https://aka.ms/tsconfig.json to read more about this file */
+
+ /* Basic Options */
+ // "incremental": true, /* Enable incremental compilation */
+ "target": "es2015",
+ /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
+ "module": "commonjs",
+ /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
+ // "lib": [], /* Specify library files to be included in the compilation. */
+ // "allowJs": true, /* Allow javascript files to be compiled. */
+ // "checkJs": true, /* Report errors in .js files. */
+ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
+ "declaration": true,
+ /* Generates corresponding '.d.ts' file. */
+ // "declarationMap": true,
+ /* Generates a sourcemap for each corresponding '.d.ts' file. */
+ // "sourceMap": true, /* Generates corresponding '.map' file. */
+ // "outFile": "./", /* Concatenate and emit output to single file. */
+ // "outDir": "dist",
+ /* Redirect output structure to the directory. */
+ "rootDirs": [
+ "src",
+ "tests"
+ ] /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
+ // "composite": true, /* Enable project compilation */
+ // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
+ // "removeComments": true, /* Do not emit comments to output. */
+ // "noEmit": true, /* Do not emit outputs. */
+ // "importHelpers": true, /* Import emit helpers from 'tslib'. */
+ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
+ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
+
+ /* Strict Type-Checking Options */
+ "strict": true,
+ /* Enable all strict type-checking options. */
+ "noImplicitAny": true,
+ /* Raise error on expressions and declarations with an implied 'any' type. */
+ "strictNullChecks": true,
+ /* Enable strict null checks. */
+ "strictFunctionTypes": true,
+ /* Enable strict checking of function types. */
+ "strictBindCallApply": true,
+ /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
+ "strictPropertyInitialization": true,
+ /* Enable strict checking of property initialization in classes. */
+ "noImplicitThis": true,
+ /* Raise error on 'this' expressions with an implied 'any' type. */
+ "alwaysStrict": true,
+ /* Parse in strict mode and emit "use strict" for each source file. */
+
+ /* Additional Checks */
+ // "noUnusedLocals": true, /* Report errors on unused locals. */
+ // "noUnusedParameters": true, /* Report errors on unused parameters. */
+ // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
+ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
+
+ /* Module Resolution Options */
+ "moduleResolution": "node",
+ /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
+ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
+ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
+ /* List of root folders whose combined content represents the structure of the project at runtime. */
+ // "typeRoots": [], /* List of folders to include type definitions from. */
+ // "types": [], /* Type declaration files to be included in compilation. */
+ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
+ "esModuleInterop": true,
+
+ /* Advanced Options */
+ "skipLibCheck": true,
+ /* Skip type checking of declaration files. */
+ "forceConsistentCasingInFileNames": true
+ /* Disallow inconsistently-cased references to the same file. */
+ }
+}
diff --git a/deno/tsconfig.json b/deno/tsconfig.json
new file mode 100644
index 0000000..4bdf2c0
--- /dev/null
+++ b/deno/tsconfig.json
@@ -0,0 +1,88 @@
+{
+ "compilerOptions": {
+ /* Visit https://aka.ms/tsconfig.json to read more about this file */
+
+ /* Basic Options */
+ // "incremental": true, /* Enable incremental compilation */
+ "target": "es2015",
+ /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
+ "module": "commonjs",
+ /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
+ // "lib": [], /* Specify library files to be included in the compilation. */
+ // "allowJs": true, /* Allow javascript files to be compiled. */
+ // "checkJs": true, /* Report errors in .js files. */
+ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
+ "declaration": true,
+ /* Generates corresponding '.d.ts' file. */
+ // "declarationMap": true,
+ /* Generates a sourcemap for each corresponding '.d.ts' file. */
+ // "sourceMap": true, /* Generates corresponding '.map' file. */
+ // "outFile": "./", /* Concatenate and emit output to single file. */
+ "outDir": "dist",
+ /* Redirect output structure to the directory. */
+ // "rootDir": ".", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
+ // "composite": true, /* Enable project compilation */
+ // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
+ // "removeComments": true, /* Do not emit comments to output. */
+ // "noEmit": true, /* Do not emit outputs. */
+ // "importHelpers": true, /* Import emit helpers from 'tslib'. */
+ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
+ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
+
+ /* Strict Type-Checking Options */
+ "strict": true,
+ /* Enable all strict type-checking options. */
+ "noImplicitAny": false,
+ /* Raise error on expressions and declarations with an implied 'any' type. */
+ "strictNullChecks": true,
+ /* Enable strict null checks. */
+ "strictFunctionTypes": true,
+ /* Enable strict checking of function types. */
+ "strictBindCallApply": true,
+ /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
+ "strictPropertyInitialization": true,
+ /* Enable strict checking of property initialization in classes. */
+ "noImplicitThis": false,
+ /* Raise error on 'this' expressions with an implied 'any' type. */
+ "alwaysStrict": true,
+ /* Parse in strict mode and emit "use strict" for each source file. */
+
+ /* Additional Checks */
+ // "noUnusedLocals": true, /* Report errors on unused locals. */
+ // "noUnusedParameters": true, /* Report errors on unused parameters. */
+ // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
+ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
+
+ /* Module Resolution Options */
+ "moduleResolution": "node",
+ /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
+ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
+ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
+ "rootDirs": ["src", "tests"],
+ /* List of root folders whose combined content represents the structure of the project at runtime. */
+ // "typeRoots": [], /* List of folders to include type definitions from. */
+ // "types": [], /* Type declaration files to be included in compilation. */
+ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
+ "esModuleInterop": true,
+ /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
+ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
+ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
+
+ /* Source Map Options */
+ // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
+ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
+ // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
+ // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
+
+ /* Experimental Options */
+ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
+ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
+
+ /* Advanced Options */
+ "skipLibCheck": true,
+ /* Skip type checking of declaration files. */
+ "forceConsistentCasingInFileNames": true
+ /* Disallow inconsistently-cased references to the same file. */
+ },
+ "exclude": ["tests", "dist", "examples", "node_modules"]
+}
diff --git a/deno/yarn.lock b/deno/yarn.lock
new file mode 100644
index 0000000..f591574
--- /dev/null
+++ b/deno/yarn.lock
@@ -0,0 +1,962 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@types/chai@^4.2.11":
+ version "4.2.11"
+ resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.11.tgz#d3614d6c5f500142358e6ed24e1bf16657536c50"
+ integrity sha512-t7uW6eFafjO+qJ3BIV2gGUyZs27egcNRkUdalkud+Qa3+kg//f129iuOFivHDXQ+vnU3fDXuwgv0cqMCbcE8sw==
+
+"@types/debug@^4.1.5":
+ version "4.1.5"
+ resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.5.tgz#b14efa8852b7768d898906613c23f688713e02cd"
+ integrity sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==
+
+"@types/ip@^1.1.0":
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/@types/ip/-/ip-1.1.0.tgz#aec4f5bfd49e4a4c53b590d88c36eb078827a7c0"
+ integrity sha512-dwNe8gOoF70VdL6WJBwVHtQmAX4RMd62M+mAB9HQFjG1/qiCLM/meRy95Pd14FYBbEDwCq7jgJs89cHpLBu4HQ==
+ dependencies:
+ "@types/node" "*"
+
+"@types/mocha@^7.0.2":
+ version "7.0.2"
+ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-7.0.2.tgz#b17f16cf933597e10d6d78eae3251e692ce8b0ce"
+ integrity sha512-ZvO2tAcjmMi8V/5Z3JsyofMe3hasRcaw88cto5etSVMwVQfeivGAlEYmaQgceUSVYFofVjT+ioHsATjdWcFt1w==
+
+"@types/node@*", "@types/node@^14.0.13":
+ version "14.0.13"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.13.tgz#ee1128e881b874c371374c1f72201893616417c9"
+ integrity sha512-rouEWBImiRaSJsVA+ITTFM6ZxibuAlTuNOCyxVbwreu6k6+ujs7DfnU9o+PShFhET78pMBl3eH+AGSI5eOTkPA==
+
+"@types/object-hash@^1.3.3":
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/@types/object-hash/-/object-hash-1.3.3.tgz#624ed28222bd5af0f936b162589c06a2b0550161"
+ integrity sha512-75t+H8u2IU1zJPPqezkGLP4YxDlj8tx7H9SgYOT1G61NjJUUEELu1Lp7RKQKXhW+FL8nV7XyD/cNFAtrKGViYQ==
+
+ansi-colors@4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
+ integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==
+
+ansi-regex@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
+ integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=
+
+ansi-regex@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
+ integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==
+
+ansi-styles@^3.2.0, ansi-styles@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
+ integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
+ dependencies:
+ color-convert "^1.9.0"
+
+anymatch@~3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142"
+ integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==
+ dependencies:
+ normalize-path "^3.0.0"
+ picomatch "^2.0.4"
+
+arg@^4.1.0:
+ version "4.1.3"
+ resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
+ integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
+
+argparse@^1.0.7:
+ version "1.0.10"
+ resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
+ integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==
+ dependencies:
+ sprintf-js "~1.0.2"
+
+array.prototype.map@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/array.prototype.map/-/array.prototype.map-1.0.2.tgz#9a4159f416458a23e9483078de1106b2ef68f8ec"
+ integrity sha512-Az3OYxgsa1g7xDYp86l0nnN4bcmuEITGe1rbdEBVkrqkzMgDcbdQ2R7r41pNzti+4NMces3H8gMmuioZUilLgw==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.17.0-next.1"
+ es-array-method-boxes-properly "^1.0.0"
+ is-string "^1.0.4"
+
+assertion-error@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b"
+ integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==
+
+balanced-match@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
+ integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
+
+binary-extensions@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c"
+ integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==
+
+blake2b-wasm@^1.1.0:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/blake2b-wasm/-/blake2b-wasm-1.1.7.tgz#e4d075da10068e5d4c3ec1fb9accc4d186c55d81"
+ integrity sha512-oFIHvXhlz/DUgF0kq5B1CqxIDjIJwh9iDeUUGQUcvgiGz7Wdw03McEO7CfLBy7QKGdsydcMCgO9jFNBAFCtFcA==
+ dependencies:
+ nanoassert "^1.0.0"
+
+blake2b@^2.1.3:
+ version "2.1.3"
+ resolved "https://registry.yarnpkg.com/blake2b/-/blake2b-2.1.3.tgz#f5388be424768e7c6327025dad0c3c6d83351bca"
+ integrity sha512-pkDss4xFVbMb4270aCyGD3qLv92314Et+FsKzilCLxDz5DuZ2/1g3w4nmBbu6nKApPspnjG7JcwTjGZnduB1yg==
+ dependencies:
+ blake2b-wasm "^1.1.0"
+ nanoassert "^1.0.0"
+
+brace-expansion@^1.1.7:
+ version "1.1.11"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+ integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
+ dependencies:
+ balanced-match "^1.0.0"
+ concat-map "0.0.1"
+
+braces@~3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
+ integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
+ dependencies:
+ fill-range "^7.0.1"
+
+browser-stdout@1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60"
+ integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==
+
+buffer-from@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
+ integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
+
+camelcase@^5.0.0:
+ version "5.3.1"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
+ integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
+
+chai-bytes@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/chai-bytes/-/chai-bytes-0.1.2.tgz#c297e81d47eb3106af0676ded5bb5e0c9f981db3"
+ integrity sha512-0ol6oJS0y1ozj6AZK8n1pyv1/G+l44nqUJygAkK1UrYl+IOGie5vcrEdrAlwmLYGIA9NVvtHWosPYwWWIXf/XA==
+
+chai@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/chai/-/chai-4.2.0.tgz#760aa72cf20e3795e84b12877ce0e83737aa29e5"
+ integrity sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==
+ dependencies:
+ assertion-error "^1.1.0"
+ check-error "^1.0.2"
+ deep-eql "^3.0.1"
+ get-func-name "^2.0.0"
+ pathval "^1.1.0"
+ type-detect "^4.0.5"
+
+chalk@^2.4.2:
+ version "2.4.2"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
+ integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
+ dependencies:
+ ansi-styles "^3.2.1"
+ escape-string-regexp "^1.0.5"
+ supports-color "^5.3.0"
+
+check-error@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82"
+ integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=
+
+chokidar@3.3.1:
+ version "3.3.1"
+ resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.1.tgz#c84e5b3d18d9a4d77558fef466b1bf16bbeb3450"
+ integrity sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg==
+ dependencies:
+ anymatch "~3.1.1"
+ braces "~3.0.2"
+ glob-parent "~5.1.0"
+ is-binary-path "~2.1.0"
+ is-glob "~4.0.1"
+ normalize-path "~3.0.0"
+ readdirp "~3.3.0"
+ optionalDependencies:
+ fsevents "~2.1.2"
+
+cliui@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5"
+ integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==
+ dependencies:
+ string-width "^3.1.0"
+ strip-ansi "^5.2.0"
+ wrap-ansi "^5.1.0"
+
+color-convert@^1.9.0:
+ version "1.9.3"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
+ integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
+ dependencies:
+ color-name "1.1.3"
+
+color-name@1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+ integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
+
+concat-map@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+ integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
+
+core-js@^3.6.5:
+ version "3.6.5"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a"
+ integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==
+
+debug@3.2.6:
+ version "3.2.6"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
+ integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
+ dependencies:
+ ms "^2.1.1"
+
+debug@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
+ integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
+ dependencies:
+ ms "^2.1.1"
+
+decamelize@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
+ integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
+
+deep-eql@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df"
+ integrity sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==
+ dependencies:
+ type-detect "^4.0.0"
+
+define-properties@^1.1.2, define-properties@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
+ integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
+ dependencies:
+ object-keys "^1.0.12"
+
+diff@4.0.2, diff@^4.0.1:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
+ integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
+
+emoji-regex@^7.0.1:
+ version "7.0.3"
+ resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
+ integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==
+
+es-abstract@^1.17.0-next.1, es-abstract@^1.17.4, es-abstract@^1.17.5:
+ version "1.17.6"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a"
+ integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==
+ dependencies:
+ es-to-primitive "^1.2.1"
+ function-bind "^1.1.1"
+ has "^1.0.3"
+ has-symbols "^1.0.1"
+ is-callable "^1.2.0"
+ is-regex "^1.1.0"
+ object-inspect "^1.7.0"
+ object-keys "^1.1.1"
+ object.assign "^4.1.0"
+ string.prototype.trimend "^1.0.1"
+ string.prototype.trimstart "^1.0.1"
+
+es-array-method-boxes-properly@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e"
+ integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==
+
+es-get-iterator@^1.0.2:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.0.tgz#bb98ad9d6d63b31aacdc8f89d5d0ee57bcb5b4c8"
+ integrity sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ==
+ dependencies:
+ es-abstract "^1.17.4"
+ has-symbols "^1.0.1"
+ is-arguments "^1.0.4"
+ is-map "^2.0.1"
+ is-set "^2.0.1"
+ is-string "^1.0.5"
+ isarray "^2.0.5"
+
+es-to-primitive@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
+ integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==
+ dependencies:
+ is-callable "^1.1.4"
+ is-date-object "^1.0.1"
+ is-symbol "^1.0.2"
+
+escape-string-regexp@1.0.5, escape-string-regexp@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+ integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
+
+esprima@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
+ integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
+
+fill-range@^7.0.1:
+ version "7.0.1"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
+ integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
+ dependencies:
+ to-regex-range "^5.0.1"
+
+find-up@4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
+ integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
+ dependencies:
+ locate-path "^5.0.0"
+ path-exists "^4.0.0"
+
+find-up@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
+ integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==
+ dependencies:
+ locate-path "^3.0.0"
+
+flat@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/flat/-/flat-4.1.0.tgz#090bec8b05e39cba309747f1d588f04dbaf98db2"
+ integrity sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==
+ dependencies:
+ is-buffer "~2.0.3"
+
+fs.realpath@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+ integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
+
+fsevents@~2.1.2:
+ version "2.1.3"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e"
+ integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==
+
+function-bind@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+ integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+
+get-caller-file@^2.0.1:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
+ integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
+
+get-func-name@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41"
+ integrity sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=
+
+glob-parent@~5.1.0:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229"
+ integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==
+ dependencies:
+ is-glob "^4.0.1"
+
+glob@7.1.6:
+ version "7.1.6"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
+ integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.0.4"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+growl@1.10.5:
+ version "1.10.5"
+ resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e"
+ integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==
+
+has-flag@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
+ integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
+
+has-flag@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
+ integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+
+has-symbols@^1.0.0, has-symbols@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
+ integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
+
+has@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+ integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+ dependencies:
+ function-bind "^1.1.1"
+
+he@1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
+ integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
+
+inflight@^1.0.4:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+ integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
+ dependencies:
+ once "^1.3.0"
+ wrappy "1"
+
+inherits@2:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+ integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+ipaddr.js@^1.9.1:
+ version "1.9.1"
+ resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
+ integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
+
+is-arguments@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3"
+ integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==
+
+is-binary-path@~2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
+ integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
+ dependencies:
+ binary-extensions "^2.0.0"
+
+is-buffer@~2.0.3:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623"
+ integrity sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==
+
+is-callable@^1.1.4, is-callable@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb"
+ integrity sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==
+
+is-date-object@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e"
+ integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==
+
+is-extglob@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+ integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
+
+is-fullwidth-code-point@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
+ integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
+
+is-glob@^4.0.1, is-glob@~4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc"
+ integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==
+ dependencies:
+ is-extglob "^2.1.1"
+
+is-map@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1"
+ integrity sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw==
+
+is-number@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+ integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
+is-regex@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.0.tgz#ece38e389e490df0dc21caea2bd596f987f767ff"
+ integrity sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==
+ dependencies:
+ has-symbols "^1.0.1"
+
+is-set@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.1.tgz#d1604afdab1724986d30091575f54945da7e5f43"
+ integrity sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA==
+
+is-string@^1.0.4, is-string@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6"
+ integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==
+
+is-symbol@^1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937"
+ integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==
+ dependencies:
+ has-symbols "^1.0.1"
+
+isarray@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
+ integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
+
+isexe@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+ integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
+
+iterate-iterator@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/iterate-iterator/-/iterate-iterator-1.0.1.tgz#1693a768c1ddd79c969051459453f082fe82e9f6"
+ integrity sha512-3Q6tudGN05kbkDQDI4CqjaBf4qf85w6W6GnuZDtUVYwKgtC1q8yxYX7CZed7N+tLzQqS6roujWvszf13T+n9aw==
+
+iterate-value@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/iterate-value/-/iterate-value-1.0.2.tgz#935115bd37d006a52046535ebc8d07e9c9337f57"
+ integrity sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ==
+ dependencies:
+ es-get-iterator "^1.0.2"
+ iterate-iterator "^1.0.1"
+
+js-yaml@3.13.1:
+ version "3.13.1"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847"
+ integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==
+ dependencies:
+ argparse "^1.0.7"
+ esprima "^4.0.0"
+
+locate-path@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
+ integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==
+ dependencies:
+ p-locate "^3.0.0"
+ path-exists "^3.0.0"
+
+locate-path@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
+ integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
+ dependencies:
+ p-locate "^4.1.0"
+
+lodash@^4.17.15:
+ version "4.17.15"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
+ integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
+
+log-symbols@3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-3.0.0.tgz#f3a08516a5dea893336a7dee14d18a1cfdab77c4"
+ integrity sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==
+ dependencies:
+ chalk "^2.4.2"
+
+make-error@^1.1.1:
+ version "1.3.6"
+ resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
+ integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
+
+minimatch@3.0.4, minimatch@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
+ integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
+ dependencies:
+ brace-expansion "^1.1.7"
+
+mocha@^8.0.1:
+ version "8.0.1"
+ resolved "https://registry.yarnpkg.com/mocha/-/mocha-8.0.1.tgz#fe01f0530362df271aa8f99510447bc38b88d8ed"
+ integrity sha512-vefaXfdYI8+Yo8nPZQQi0QO2o+5q9UIMX1jZ1XMmK3+4+CQjc7+B0hPdUeglXiTlr8IHMVRo63IhO9Mzt6fxOg==
+ dependencies:
+ ansi-colors "4.1.1"
+ browser-stdout "1.3.1"
+ chokidar "3.3.1"
+ debug "3.2.6"
+ diff "4.0.2"
+ escape-string-regexp "1.0.5"
+ find-up "4.1.0"
+ glob "7.1.6"
+ growl "1.10.5"
+ he "1.2.0"
+ js-yaml "3.13.1"
+ log-symbols "3.0.0"
+ minimatch "3.0.4"
+ ms "2.1.2"
+ object.assign "4.1.0"
+ promise.allsettled "1.0.2"
+ serialize-javascript "3.0.0"
+ strip-json-comments "3.0.1"
+ supports-color "7.1.0"
+ which "2.0.2"
+ wide-align "1.1.3"
+ workerpool "6.0.0"
+ yargs "13.3.2"
+ yargs-parser "13.1.2"
+ yargs-unparser "1.6.0"
+
+ms@2.1.2, ms@^2.1.1:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
+ integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
+
+nanoassert@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/nanoassert/-/nanoassert-1.1.0.tgz#4f3152e09540fde28c76f44b19bbcd1d5a42478d"
+ integrity sha1-TzFS4JVA/eKMdvRLGbvNHVpCR40=
+
+normalize-path@^3.0.0, normalize-path@~3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
+ integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+
+object-hash@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.0.3.tgz#d12db044e03cd2ca3d77c0570d87225b02e1e6ea"
+ integrity sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg==
+
+object-inspect@^1.7.0:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0"
+ integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==
+
+object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
+ integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
+
+object.assign@4.1.0, object.assign@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da"
+ integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==
+ dependencies:
+ define-properties "^1.1.2"
+ function-bind "^1.1.1"
+ has-symbols "^1.0.0"
+ object-keys "^1.0.11"
+
+once@^1.3.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+ integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
+ dependencies:
+ wrappy "1"
+
+p-limit@^2.0.0, p-limit@^2.2.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
+ integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
+ dependencies:
+ p-try "^2.0.0"
+
+p-locate@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
+ integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==
+ dependencies:
+ p-limit "^2.0.0"
+
+p-locate@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
+ integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
+ dependencies:
+ p-limit "^2.2.0"
+
+p-try@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
+ integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
+
+path-exists@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
+ integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=
+
+path-exists@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
+ integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
+
+path-is-absolute@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+ integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
+
+pathval@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0"
+ integrity sha1-uULm1L3mUwBe9rcTYd74cn0GReA=
+
+picomatch@^2.0.4, picomatch@^2.0.7:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
+ integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==
+
+prettier@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.5.tgz#d6d56282455243f2f92cc1716692c08aa31522d4"
+ integrity sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==
+
+promise.allsettled@1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/promise.allsettled/-/promise.allsettled-1.0.2.tgz#d66f78fbb600e83e863d893e98b3d4376a9c47c9"
+ integrity sha512-UpcYW5S1RaNKT6pd+s9jp9K9rlQge1UXKskec0j6Mmuq7UJCvlS2J2/s/yuPN8ehftf9HXMxWlKiPbGGUzpoRg==
+ dependencies:
+ array.prototype.map "^1.0.1"
+ define-properties "^1.1.3"
+ es-abstract "^1.17.0-next.1"
+ function-bind "^1.1.1"
+ iterate-value "^1.0.0"
+
+readdirp@~3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.3.0.tgz#984458d13a1e42e2e9f5841b129e162f369aff17"
+ integrity sha512-zz0pAkSPOXXm1viEwygWIPSPkcBYjW1xU5j/JBh5t9bGCJwa6f9+BJa6VaB2g+b55yVrmXzqkyLf4xaWYM0IkQ==
+ dependencies:
+ picomatch "^2.0.7"
+
+require-directory@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+ integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
+
+require-main-filename@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
+ integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
+
+serialize-javascript@3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-3.0.0.tgz#492e489a2d77b7b804ad391a5f5d97870952548e"
+ integrity sha512-skZcHYw2vEX4bw90nAr2iTTsz6x2SrHEnfxgKYmZlvJYBEZrvbKtobJWlQ20zczKb3bsHHXXTYt48zBA7ni9cw==
+
+set-blocking@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
+ integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
+
+source-map-support@^0.5.17:
+ version "0.5.19"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
+ integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
+ dependencies:
+ buffer-from "^1.0.0"
+ source-map "^0.6.0"
+
+source-map@^0.6.0:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+ integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
+sprintf-js@~1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
+ integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
+
+"string-width@^1.0.2 || 2":
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
+ integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
+ dependencies:
+ is-fullwidth-code-point "^2.0.0"
+ strip-ansi "^4.0.0"
+
+string-width@^3.0.0, string-width@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
+ integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==
+ dependencies:
+ emoji-regex "^7.0.1"
+ is-fullwidth-code-point "^2.0.0"
+ strip-ansi "^5.1.0"
+
+string.prototype.trimend@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913"
+ integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.17.5"
+
+string.prototype.trimstart@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54"
+ integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.17.5"
+
+strip-ansi@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
+ integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8=
+ dependencies:
+ ansi-regex "^3.0.0"
+
+strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
+ integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
+ dependencies:
+ ansi-regex "^4.1.0"
+
+strip-json-comments@3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7"
+ integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==
+
+supports-color@7.1.0:
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1"
+ integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==
+ dependencies:
+ has-flag "^4.0.0"
+
+supports-color@^5.3.0:
+ version "5.5.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
+ integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
+ dependencies:
+ has-flag "^3.0.0"
+
+to-regex-range@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+ integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+ dependencies:
+ is-number "^7.0.0"
+
+ts-node@^8.10.2:
+ version "8.10.2"
+ resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.10.2.tgz#eee03764633b1234ddd37f8db9ec10b75ec7fb8d"
+ integrity sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA==
+ dependencies:
+ arg "^4.1.0"
+ diff "^4.0.1"
+ make-error "^1.1.1"
+ source-map-support "^0.5.17"
+ yn "3.1.1"
+
+tweetnacl@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596"
+ integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==
+
+type-detect@^4.0.0, type-detect@^4.0.5:
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
+ integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
+
+typescript@^3.9.5:
+ version "3.9.5"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.5.tgz#586f0dba300cde8be52dd1ac4f7e1009c1b13f36"
+ integrity sha512-hSAifV3k+i6lEoCJ2k6R2Z/rp/H3+8sdmcn5NrS3/3kE7+RyZXm9aqvxWqjEXHAd8b0pShatpcdMTvEdvAJltQ==
+
+which-module@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
+ integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
+
+which@2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
+ integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
+ dependencies:
+ isexe "^2.0.0"
+
+wide-align@1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457"
+ integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==
+ dependencies:
+ string-width "^1.0.2 || 2"
+
+workerpool@6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.0.0.tgz#85aad67fa1a2c8ef9386a1b43539900f61d03d58"
+ integrity sha512-fU2OcNA/GVAJLLyKUoHkAgIhKb0JoCpSjLC/G2vYKxUjVmQwGbRVeoPJ1a8U4pnVofz4AQV5Y/NEw8oKqxEBtA==
+
+wrap-ansi@^5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09"
+ integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==
+ dependencies:
+ ansi-styles "^3.2.0"
+ string-width "^3.0.0"
+ strip-ansi "^5.0.0"
+
+wrappy@1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+ integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
+
+y18n@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
+ integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==
+
+yargs-parser@13.1.2, yargs-parser@^13.1.2:
+ version "13.1.2"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38"
+ integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==
+ dependencies:
+ camelcase "^5.0.0"
+ decamelize "^1.2.0"
+
+yargs-unparser@1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-1.6.0.tgz#ef25c2c769ff6bd09e4b0f9d7c605fb27846ea9f"
+ integrity sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==
+ dependencies:
+ flat "^4.1.0"
+ lodash "^4.17.15"
+ yargs "^13.3.0"
+
+yargs@13.3.2, yargs@^13.3.0:
+ version "13.3.2"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd"
+ integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==
+ dependencies:
+ cliui "^5.0.0"
+ find-up "^3.0.0"
+ get-caller-file "^2.0.1"
+ require-directory "^2.1.1"
+ require-main-filename "^2.0.0"
+ set-blocking "^2.0.0"
+ string-width "^3.0.0"
+ which-module "^2.0.0"
+ y18n "^4.0.0"
+ yargs-parser "^13.1.2"
+
+yn@3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
+ integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==