Like JSON-RPC, but supports streaming.

Overview

Earthstar Streaming RPC

Similar to JSON-RPC, but also supports streaming (soon). Written to be used in Earthstar (github, docs).

Table of Contents

Usage

Importing

To use in Deno you can import directly from Github using a specific git tag as a version number:

import {
    TransportHttpClient,
    TransportHttpServer,
} from 'https://raw.githubusercontent.com/earthstar-project/earthstar-streaming-rpc/v2.0.0/mod.ts';

To use with Node or apps built with NPM dependencies:

npm install earthstar-streaming-rpc

And then import in your code:

import { TransportHttpClient, TransportHttpServer } from 'earthstar-streaming-rpc';

Concepts

A Transport represents a certain kind of network connection. It's responsible for managing Connections with other devices. There are many flavors of Transport class, but only one kind of Connection class. A Transport can represent a client-server sort of network connection like HTTP, or a symmetrical p2p one like hyperswarm.

A Connection instance is a 1-to-1 relationship with another device, and gives you a way to call methods on the other device.

Connections are symmetrical, regardless of the underlying client-server nature of the network connection -- either side of the connection can call methods on each other.

API Example

Look in types.ts for more details.

Let's define some methods that you want to expose to other devices. Think carefully about security with these!

You can also use a class here instead of an object-of-functions. You can use sync or async functions.

NOTE: the arguments and return value must all be JSON-serializable. In particular this means you can't use undefined anywhere -- use null instead.

const methods = {
    add: (a: number, b: number) => a + b,
    greetSlowly: async (name: string) => {
        await sleep(1000);
        return 'Hello ' + name;
    },
};

Create a Transport for the kind of network connecton you want to use. You are responsible for coming up with a random deviceId. It can be different each time your code runs, but if you have several Transports running at the same time on your device, use the same deviceId on each one.

// Each kind of Transport has its own unique constructor
const transport = new TransportHttpClient({
    deviceId: 'o93idjodo2i3jd',
    methods,
});

Create a Connection to the other device.

// Each kind of Transport has its own way of doing this.
const conn = transport.addConnection('http://example.com/api/v1');

// Server-side transports don't let you create Connections on demand --
// they sit and wait for connections to arrive.
// You can grab the existing connections from the WatchableSet
// at transport.connections, or subscribe to changes
for (const conn of transport.connections) {
    /* ... */
}
transport.connections.onAdd((conn) => {/* ... */});
transport.connections.onDelete((conn) => {/* ... */});
transport.connections.onChange((conn) => {/* ... */});

Use the Connection to call methods on the other device. There are 3 ways to do this:

  • notify -- call the method but don't wait for the result
  • request -- call the method and wait for the result to come back
  • stream -- TODO: start a stream (not implemented yet)
// This does not return an answer.
await conn.notify('greetSlowly', 'Suzy');

// This waits for the returned value.
const three = await conn.request('add', 1, 2);

Closing things

Connections have a status which can be CONNECTING, OPEN, ERROR, or CLOSED. The possible sequence of states is:

CONNECTING --> (OPEN | ERROR)* --> CLOSED

examples
CONNECTING --> CLOSED
CONNECTING --> OPEN --> CLOSED
CONNECTING --> ERROR --> CLOSED
CONNECTING --> OPEN --> ERROR --> OPEN --> ERROR --> CLOSED

ERROR means the network connection failed. It will try to reconnect and become OPEN again.

It won't become CLOSED until you ask it to; once CLOSED it can't be used anymore or re-opened (make a new one instead).

status is a Watchable -- you can subscribe to changes, and you have to use get() to get the value. Don't set it yourself.

// using the connection status

console.log(connection.status.get());

connection.status.onChange((oldVal, newVal) => {/* ... */});

connection.status.onChangeTo('CLOSED', (oldVal, newVal) => {/* ... */});

connection.close();

The Transport also has a status but it can only be OPEN or CLOSED.

It won't become CLOSED until you ask it to; once CLOSED it can't be used anymore or re-opened (make a new one instead).

OPEN --> CLOSED

When you're done, be sure to close the Transport. Otherwise it might have some timers running which will prevent Deno or Node from exiting. Closing the Transport will close all the Connections for you.

transport.close();

Error handling

If there's a network problem:

  • connection.status will become ERROR
  • connection.notify and connection.request will throw errors, such as RpcErrorTimeout or RpcErrorNetworkProblem or a built-in error type related to the network
  • The Connection will try to reconnect and become OPEN again
  • The Transport will remain OPEN (TODO?)

If you call a method name that does not exist on the other side:

  • connection.notify will do nothing
  • connection.request will throw a RpcErrorUnknownMethod

If your method call crashes on the other side, because...

  • ...the method threw an error on purpose
  • ...the method crashed for some reason
  • ...the arguments you provided did not make sense (note that nothing checks if you provided the correct number of arguments, it just tries to run the method as you asked)

then...

  • connection.notify will do nothing
  • connection.request will throw a RpcErrorFromMethod with a stringified version of the original error

If you try to do anything with a Connection or Transport that is CLOSED:

  • a RpcErrorUseAfterClose error will be thrown

Transport classes

HTTP

TransportHttpClient

TransportHttpServer (Any runtime / server with support for the Fetch API's Request and Response)

TransportHttpServerOpine (Deno)

TransportHttpServerExpress (Node)

This is a lazy way to get bidirectional communication over HTTP. We'll improve it later:

Client --> server: messages are POSTed in batches (arrays), currently one at a time, so the array always has length 1. This should be improved by batching up messages and sending them every 50 milliseconds.

Server --> client: The client does GET requests to poll for batches of messages that the server has accumulated for that particular client (by deviceId). It polls quickly until the server is empty, then it slows down and polls every couple of seconds. This should be converted to a single streaming GET.

Other

TransportLocal - A connection within one device, mostly useful for testing.

Future transport types, not written yet

  • BroadcastChannel, for communicating between browser tabs
  • Websockets
  • Hyperswarm

Writing a new kind of Transport class

Behind the scenes, each method call (or response) is wrapped in an Envelope object which is then (de)serialized to JSON.

TODO: write more

Packaging & Building

This is a "Deno-first" package -- it's written for Deno, and then also converted and published to npm. It should also work from browsers -- typically you'd import it from your own separate npm project and use a bundler such as webpack to put everything together.

Code structure

Just the important things:

Everything:

Development

Setup

You will need Deno installed. Instructions for installation can be found here. You may also want type-checking and linting from Deno for your IDE, which you can get with extensions like this one for VSCode.

To check that you've got everything set up correctly:

make example

This will run the example script at example-app.ts, and you will see a lot of colourful log messages from the app.

Scripts

Scripts are run with the make command.

  • make test - Run all tests
  • make test-watch - Run all tests in watch mode
  • make fmt - Format all code in the codebase
  • make npm - Create a NPM package in npm and run tests against it (requires Node v14 or v16 to be installed).
  • make bundle - Create a bundled browser script at earthstar.bundle.js
  • make depchart - Regenerate the dependency chart images
  • make coverage - Generate code test coverage statistics
  • make clean - Delete generated files

Where to find things

  • The entry for the package can be found at mod.ts.
  • Most external dependencies can be found in deps.ts. All other files import external dependencies from this file.
  • Script definitions can be found in Makefile.
  • Tests are all in src/test/
  • The script for building the NPM package can be found in scripts/build_npm.ts

Publishing to NPM

  1. Run make VERSION="version.number.here" npm, where version.number.here is the desired version number for the package.
  2. cd npm
  3. npm publish
Comments
  • Add TransportHttpServer + Express + Opine, test scenarios

    Add TransportHttpServer + Express + Opine, test scenarios

    What's the problem you solved?

    The TransportHttpServer class is only useful for Opine / Express servers.

    What solution are you recommending?

    I've added a TransportHttpHandler class which has a handler property, a function with a signature of (req: Request) => Promise<Response>, both classes from the Fetch standard.

    This function checks whether the request matches a specific URL pattern (e.g. /from/:otherDeviceId), executes the appropriate logic (e.g. getting a connection with a certain device, handling an envelope), and returns the result as a response.

    Then you can do stuff like this:

    import { serve } from "https://deno.land/[email protected]/http/server.ts";
    
    const httpHandler = new SyncerHttpHandler(myPeer, "/");
    
    await serve(httpHandler.handler);
    

    And go on to compose the handler to intercept requests for a certain page, stuff like that.

    I've also been thinking about how the handler could be composed into Opine / Express servers, but weirdly enough there are seemingly no utilities for converting express Requests to standard Requests, or standard Responses to express responses. If we had those, then you could do this:

    app.on('*', async (req, res) => {
      const standardReq = toFetchReq(req);
    
      const standardRes = await httpHandler.handler(standardReq);
    
      applyStandardRes(res, standardRes);
    });
    

    Something like that.

    Ultimately I think it's less work for us if we don't include an express / opine server with this library (and by extension, Earthstar...), but without a good way to convert standard fetch requests and results between frameworks that's a non-starter.

    opened by sgwilym 7
  • Allow HTTP transport servers to use path opt if present

    Allow HTTP transport servers to use path opt if present

    What's the problem you want solved?

    transport-http-server-opine (and -express) currently hijack all of the routes of the opine/express app that is passed into the constructor

    Is there a solution you'd like to recommend?

    Use the path opt if it exists and if not default to the wildcard

    opened by AnActualEmerald 1
  • Add stronger types via generics

    Add stronger types via generics

    What's the problem you solved?

    Currently the following issues are not caught by Typescript:

    • Calling a non-existent method on a Connection#notify or Connection#request
    • Providing arguments of the wrong type or length to Connection#notify or Connection#request

    And the following types return vague types or any:

    • Transport.methods
    • EnvelopeResponseWithData.data
    • EnvelopeRequest.method
    • EnvelopeRequest.args

    What solution are you recommending?

    CleanShot 2022-01-26 at 15 05 32

    We can infer the types of all these things because we know the type of the bag of methods being passed around. I've added generic typings to many classes so that these errors are caught — I hope it will save us from many little issues as we integrate this library with Earthstar!

    I know that it adds a lot of angle brackets, but I hope the improvement in compiler feedback is worth it.

    opened by sgwilym 1
  • RPC: how to import express in Deno (or use opine?)

    RPC: how to import express in Deno (or use opine?)

    Help set up the project so it imports express in node, but opine in deno. Hopefully they are similar enough that our basic usage of them will work in both cases.

    Nothing is importing this yet, it will be part of TransportHttpServer when that is written.

    testing and tooling 
    opened by cinnamon-bun 1
  • Refactor TransportHttpClient to use a state machine... kinda...

    Refactor TransportHttpClient to use a state machine... kinda...

    What's the problem you solved?

    I had turned TransportHttpClient into a ball of timeout spaghetti, which not only made it very hard to read but also meant that it was leaking timers.

    What solution are you recommending?

    CleanShot 2022-02-13 at 22 23 13@2x

    I have refactored the 'pull' part of the Http client's into three classes representing the three possible states a pull can be in:

    • InFlightPullState
    • ScheduledPullState
    • ClosedPullState

    These classes have methods on them which allow them to transition to other valid states (e.g. ScheduledPullState -> InFlightPullState is fine, ClosedPullState -> ScheduledPullState is not!)

    This makes the 'pull' part of the client a lot easier to understand and debug, and also makes it not leak anymore.

    opened by sgwilym 0
  • Add Websocket transports

    Add Websocket transports

    What's the problem you solved?

    There were no transportsusing Websockets, which was a shame because the Websocket API is well suited to our RPC library.

    What solution are you recommending?

    @cinnamon-bun built two nice transports using the Websocket API: one for the client and one for the server. I just added tests and a few tiny fixes.

    Availability:

    Deno: TransportWebsocketClient, TransportWebsocketServer Web: TransportWebsocketClient Node: TransportWebsocketClient (Server will come later: Websocket will be shimmed at some point by https://github.com/denoland/dnt/issues/49)

    Closes #2

    opened by sgwilym 0
  • Add TransportBroadcastChannel

    Add TransportBroadcastChannel

    What's the problem you solved?

    There was no transport for communication between windows or tabs on the same origin! Weird!

    What solution are you recommending?

    I've added a TransportBroadcastChannel transport. It's a bit like TransportLocal in that it has neither a server or client role, it's just one class. You can have multiple transports join the same broadcast channel without the devices getting their messages mixed up for each other. It's fun!

    There is a flakey test in here though. Something is going wrong with a promise, and I can't track it down. @cinnamon-bun Is there anything I'm missing in my implementation that could cause something like that?

    Closes #4

    opened by sgwilym 0
  • "EventTarget is not defined" on node 14

    What's the problem you want solved?

    npm test fails on node 14:

    ReferenceError: EventTarget is not defined
        at Object.<anonymous> (/Users/me/projects/earthstar-streaming-rpc/npm/node_modules/@deno/shim-deno/dist/deno/stable/classes/PermissionStatus.js:7:32)
    

    This is coming from denoland/node_deno_shims also known as @deno/shim-deno, used by dnt.

    This was fixed already in deno shims but the fix has not been released, it came a few days after the latest release of 0.1.2.

    Is there a solution you'd like to recommend?

    We need to encourage shim-deno to make a new npm release.

    testing and tooling 
    opened by cinnamon-bun 0
  • Watchable set

    Watchable set

    What's the problem you solved?

    Users of this API need a way to know when a Transport has added or removed a Connection. This isn't really needed for client-side transports since you have to ask them to add a connection in the first place, but it's need for server-side transports since they accept connections on their own.

    What solution are you recommending?

    transport.connections has been changed from an array to a WatchableSet.

    A WatchableSet is a subclass of Set, with added methods for subscribing to add, delete, and change events.

    transport.connections.onAdd(conn => { /* ... */ });
    
    opened by cinnamon-bun 0
  • Resolve HTTP not working for both Browser and Node in NPM package.

    Resolve HTTP not working for both Browser and Node in NPM package.

    What's the problem you want solved?

    When the NPM package is bundled, fetch is shimmed using node-fetch. This shim does not work in the browser, which means that users of the NPM versions of earthstar packages cannot sync via HTTP.

    Is there a solution you'd like to recommend?

    I need to find some solution for either:

    • Shimming with an 'isomorphic' version of fetch which works in both Node and the Browser. I'd probably need to make this myself as there are no packages right now which satisfy dnt's ESM and typings requirements.
    • Possibly splitting build process up so that there is both a Node and Browser version of earthstar-streaming-rpc, further complicating things.
    opened by sgwilym 0
  • RPC: get tests running in various browsers

    RPC: get tests running in various browsers

    Make the tests run in a browser somehow. Ideally multiple specific browsers (Chrome, Firefox).

    Possibly solutions: browser-run, Jest, Puppeteer, Playwright

    testing and tooling 
    opened by cinnamon-bun 0
Owner
Earthstar Project
Earthstar Project
Get JSON RPC on a stream

json-rpc-on-a-stream Get JSON RPC on a stream npm install json-rpc-on-a-stream

Mathias Buus 13 May 31, 2022
⚡️ lowdb is a small local JSON database powered by Lodash (supports Node, Electron and the browser)

Lowdb Small JSON database for Node, Electron and the browser. Powered by Lodash. ⚡ db.get('posts') .push({ id: 1, title: 'lowdb is awesome'}) .wri

null 18.9k Dec 30, 2022
A JSON Database that saves your Json data in a file and makes it easy for you to perform CRUD operations.

What is dbcopycat A JSON Database that saves your Json data in a file and makes it easy for you to perform CRUD operations. ⚡️ Abilities Creates the f

İsmail Can Karataş 13 Jan 8, 2023
A transparent, in-memory, streaming write-on-update JavaScript database for Small Web applications that persists to a JavaScript transaction log.

JavaScript Database (JSDB) A zero-dependency, transparent, in-memory, streaming write-on-update JavaScript database for the Small Web that persists to

Small Technology Foundation 237 Nov 13, 2022
Streaming and playing on the Nintendo Switch remotely!

Switch-Stream This is a semi-convoluted application as a proof-of-concept that someone could play their Switch from a distance. A server is connected

Charles Zawacki 8 May 2, 2022
An example repository on how to start building graph applications on streaming data. Just clone and start building 💻 💪

Example Streaming App ?? ?? This repository serves as a point of reference when developing a streaming application with Memgraph and a message broker

Memgraph 40 Dec 20, 2022
HLS, DASH, and future HTTP streaming protocols library for video.js

videojs-http-streaming (VHS) Play HLS, DASH, and future HTTP streaming protocols with video.js, even where they're not natively supported. Included in

Video.js 2.2k Jan 5, 2023
ORM for TypeScript and JavaScript (ES7, ES6, ES5). Supports MySQL, PostgreSQL, MariaDB, SQLite, MS SQL Server, Oracle, SAP Hana, WebSQL databases. Works in NodeJS, Browser, Ionic, Cordova and Electron platforms.

TypeORM is an ORM that can run in NodeJS, Browser, Cordova, PhoneGap, Ionic, React Native, NativeScript, Expo, and Electron platforms and can be used

null 30.1k Jan 3, 2023
TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, MariaDB, PostgreSQL and SQLite databases.

TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, MariaDB, PostgreSQL and SQLite datab

MikroORM 5.4k Dec 31, 2022
A web client port-scanner written in GO, that supports the WASM/WASI interface for Browser WebAssembly runtime execution.

WebAssembly Port Scanner Written in Go with target WASM/WASI. The WASM main function scans all the open ports in the specified range (see main.go), vi

Avi Lumelsky 74 Dec 27, 2022
A lightweight IDE that supports verilog simulation and Risc-V code compilation

EveIDE_LIGHT 使用手册 当前版本 : v0.0.2-beta (支持windows7以上版本 64位操作系统) 作者 : Adancurusul 版本说明 版本部分特性 概述 什么是EveIDE_LIGHT 使用介绍 选择工作区 进入主界面 左侧模组区 工程视图 编译设置 仿真设置 右侧

Chen Yuheng 43 Aug 29, 2022
The Cassandra/Scylla library you didn't want but got anyways.

Installation Using npm: npm install scyllo or if you prefer to use the yarn package manager: yarn add scyllo Usage import { ScylloClient } from 'scyll

LVK.SH 7 Jul 20, 2022
Same as sqlite-tag but without the native sqlite3 module dependency

sqlite-tag-spawned Social Media Photo by Tomas Kirvėla on Unsplash The same sqlite-tag ease but without the native sqlite3 dependency, aiming to repla

Andrea Giammarchi 17 Nov 20, 2022
AlaSQL.js - JavaScript SQL database for browser and Node.js. Handles both traditional relational tables and nested JSON data (NoSQL). Export, store, and import data from localStorage, IndexedDB, or Excel.

Please use version 1.x as prior versions has a security flaw if you use user generated data to concat your SQL strings instead of providing them as a

Andrey Gershun 6.1k Jan 9, 2023
PathQL is a json standard based on GraphQL to build simple web applications.

PathQL Usage You can simple create a new PathQL Entry, which allows you to automize database over an orm and client requests over the PathQL JSON Requ

Nowhere 3 Jul 20, 2022
A database library stores JSON file for Node.js.

concisedb English | 简体中文 A database library stores JSON file for Node.js. Here is what updated every version if you want to know. API Document Usage B

LKZ烂裤子 3 Sep 4, 2022
Database in JSON, fast and optimize

?? Database-Dev ?? DevNetwork#2103 ?? V 1.0.0 ?? Dependence Libs use ?? NodeJs V 16.14.2 (NodeJs) ?? fs (nodeFS) ?? Use libs import the file in your p

DevNetwork™️ 3 Apr 21, 2022
Lovefield is a relational database for web apps. Written in JavaScript, works cross-browser. Provides SQL-like APIs that are fast, safe, and easy to use.

Lovefield Lovefield is a relational database written in pure JavaScript. It provides SQL-like syntax and works cross-browser (currently supporting Chr

Google 6.8k Jan 3, 2023
A MongoDB-like database built on top of Hyperbee with support for indexing

hyperbeedeebee A MongoDB-like database built on top of Hyperbee with support for indexing WIP: There may be breaking changes in the indexing before th

null 35 Dec 12, 2022