From 27bf62b07b97571342841e8eb8e5f5c9730c0484 Mon Sep 17 00:00:00 2001 From: Daniel Nagy <1622446+daniel-nagy@users.noreply.github.com> Date: Mon, 15 Jan 2024 13:27:31 -0600 Subject: [PATCH] Beta 5 (#6) --- README.md | 39 +++++---- packages/browser/src/BroadcastSubject.ts | 2 +- .../browser/src/StructuredCloneable.test.ts | 6 +- .../src/StructuredCloneable.typetest.ts | 2 +- packages/core/README.md | 84 +++++++++---------- packages/core/src/Session.test.ts | 4 +- packages/core/src/Session.ts | 38 ++++----- packages/core/src/Session.typetest.ts | 4 +- packages/core/src/Subprotocol.ts | 29 +++---- 9 files changed, 104 insertions(+), 104 deletions(-) diff --git a/README.md b/README.md index 4c38790..c91c884 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ Here's our `User` module. const users = [ { id: 0, name: "Dan" }, { id: 1, name: "Jessica" }, - { id: 2, name: "Mike" } + { id: 2, name: "Mike" }, ]; export async function list() { @@ -94,22 +94,22 @@ import * as Subprotocol from "@daniel-nagy/transporter/Subprotocol"; import * as User from "./User"; const Api = { - User + User, }; export type Api = typeof Api; const protocol = Subprotocol.init({ connectionMode: Subprotocol.ConnectionMode.Connectionless, + dataType: Subprotocol.Datatype(), operationMode: Subprotocol.OperationMode.Unicast, - protocol: Subprotocol.Protocol(), - transmissionMode: Subprotocol.TransmissionMode.HalfDuplex + transmissionMode: Subprotocol.TransmissionMode.HalfDuplex, }); const session = Session.server({ protocol, provide: Api }); ``` -For now don't worry about the different modes and just focus on the data format. In this case we are telling Transporter that our API only uses JSON data types. With strict type checking enabled we get a type error. +For now don't worry about the different modes and just focus on the data type. In this case we are telling Transporter that our API only uses JSON data types. With strict type checking enabled we get a type error. ``` Type 'undefined' is not assignable to type 'Json'. @@ -121,8 +121,8 @@ Can you spot the problem? If you can't then don't worry because the compiler spo - import * as Json from "@daniel-nagy/transporter/Json"; + import * as SuperJson from "@daniel-nagy/transporter/SuperJson"; -- protocol: Subprotocol.Protocol(), -+ protocol: Subprotocol.Protocol(), +- dataType: Subprotocol.DataType(), ++ dataType: Subprotocol.DataType(), ``` With that change the error will go away. @@ -140,14 +140,14 @@ import type { Api } from "./Server"; const protocol = Subprotocol.init({ connectionMode: Subprotocol.ConnectionMode.Connectionless, + dataType: Subprotocol.DataType(), operationMode: Subprotocol.OperationMode.Unicast, - protocol: Subprotocol.Protocol(), - transmissionMode: Subprotocol.TransmissionMode.HalfDuplex + transmissionMode: Subprotocol.TransmissionMode.HalfDuplex, }); const session = Session.client({ protocol, - resource: Session.Resource() + resource: Session.Resource(), }); const client = session.createProxy(); @@ -174,11 +174,11 @@ Bun.serve({ async fetch(req) { using session = Session.server({ protocol, provide: Api }); const reply = Observable.firstValueFrom(session.output); - const message = SuperJson.fromJson(await req.json()) + const message = SuperJson.fromJson(await req.json()); session.input.next(message as Message.t); return Response.json(SuperJson.toJson(await reply)); }, - port: 3000 + port: 3000, }); ``` @@ -195,27 +195,27 @@ const session = Session.client({ protocol, resource: Session.Resource(), }); - + const toRequest = (message: string) => new Request("http://localhost:3000", { body: message, headers: { - "Content-Type": "application/json" + "Content-Type": "application/json", }, method: "POST", }); - + session.output .pipe( Observable.map(SuperJson.toJson), Observable.map(JSON.stringify), Observable.map(toRequest), Observable.flatMap(fetch), - Observable.flatMap(response => response.json()), + Observable.flatMap((response) => response.json()), Observable.map(SuperJson.fromJson) ) .subscribe(session.input); - + const client = session.createProxy(); ``` @@ -232,9 +232,9 @@ It is important to make sure your subprotocol and your transport layer are compa ```ts const protocol = Subprotocol.init({ connectionMode: Subprotocol.ConnectionMode.ConnectionOriented, + dataType: Subprotocol.DataType(), operationMode: Subprotocol.OperationMode.Unicast, - protocol: Subprotocol.Protocol(), - transmissionMode: Subprotocol.TransmissionMode.Duplex + transmissionMode: Subprotocol.TransmissionMode.Duplex, }); ``` @@ -271,4 +271,3 @@ This example uses the `BrowserServer` API to communicate with a service worker. This example renders a webview with a button to scan a barcode. When the button is tapped it will use the `BarCodeScanner` component from Expo to access the camera to scan a barcode. Because this example uses the camera you will need to run it on a real device. I just use the Expo Go app on my phone. Transporter does not currently offer any React Native specific APIs. However, I may add React Native specific APIs similar to the browser APIs. It's just that React Native can be..._time consuming_. - diff --git a/packages/browser/src/BroadcastSubject.ts b/packages/browser/src/BroadcastSubject.ts index 864ee9c..0fa3357 100644 --- a/packages/browser/src/BroadcastSubject.ts +++ b/packages/browser/src/BroadcastSubject.ts @@ -11,8 +11,8 @@ type Observer = Required>; const protocol = Subprotocol.init({ connectionMode: Subprotocol.ConnectionMode.Connectionless, + dataType: Subprotocol.DataType(), operationMode: Subprotocol.OperationMode.Broadcast, - protocol: Subprotocol.Protocol(), transmissionMode: Subprotocol.TransmissionMode.Simplex }); diff --git a/packages/browser/src/StructuredCloneable.test.ts b/packages/browser/src/StructuredCloneable.test.ts index 85d04b8..6f05299 100644 --- a/packages/browser/src/StructuredCloneable.test.ts +++ b/packages/browser/src/StructuredCloneable.test.ts @@ -377,8 +377,8 @@ test("memoizing a proxied function", async () => { const protocol = Subprotocol.init({ connectionMode: Subprotocol.ConnectionMode.ConnectionOriented, + dataType: Subprotocol.DataType(), operationMode: Subprotocol.OperationMode.Unicast, - protocol: Subprotocol.Protocol(), transmissionMode: Subprotocol.TransmissionMode.Duplex }); @@ -396,13 +396,13 @@ function expose( } = {} ) { const client = Session.client({ - protocol: protocol, + protocol, resource: Session.Resource() }); const server = Session.server({ ...serverConfig, - protocol: protocol, + protocol, provide: value }); diff --git a/packages/browser/src/StructuredCloneable.typetest.ts b/packages/browser/src/StructuredCloneable.typetest.ts index 6b87b27..b61e9d8 100644 --- a/packages/browser/src/StructuredCloneable.typetest.ts +++ b/packages/browser/src/StructuredCloneable.typetest.ts @@ -6,8 +6,8 @@ import * as Subprotocol from "../../../node_modules/@daniel-nagy/transporter/src const protocol = Subprotocol.init({ connectionMode: Subprotocol.ConnectionMode.ConnectionOriented, + dataType: Subprotocol.DataType(), operationMode: Subprotocol.OperationMode.Unicast, - protocol: Subprotocol.Protocol(), transmissionMode: Subprotocol.TransmissionMode.Duplex }); diff --git a/packages/core/README.md b/packages/core/README.md index dfca148..eb2966d 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -479,7 +479,7 @@ const getUser = Injector.provide( _Module_ -A `Json` type may be used as a subprotocol. If both ends of your communication channel are JavaScript runtimes then you may use the [SuperJson](#Superjson) module instead for a much larger set of types. +A `Json` type may be used as a data type for a subprotocol. If both ends of your communication channel are JavaScript runtimes then you may use the [SuperJson](#Superjson) module instead for a much larger set of types. ###### Types @@ -1642,9 +1642,9 @@ Transporter protocol. _Type_ ```ts -interface ClientOptions { +interface ClientOptions { injector?: Injector.t; - protocol: Subprotocol; + protocol: Subprotocol; resource: Resource; } ``` @@ -1656,7 +1656,7 @@ Options for creating a `ClientSession`. _Type_ ```ts -class ClientSession extends Session { +class ClientSession extends Session { createProxy(): Proxy.t; } ``` @@ -1678,9 +1678,9 @@ A `Resource` is a value that is provided by a server. _Type_ ```ts -interface ServerOptions { +interface ServerOptions { injector?: Injector.t; - protocol: Subprotocol; + protocol: Subprotocol; provide: Value; } ``` @@ -1692,7 +1692,7 @@ Options for creating a `ServerSession`. _Type_ ```ts -class ServerSession extends Session {} +class ServerSession extends Session {} ``` A `ServerSession` is created on the server to provide a resource. @@ -1702,11 +1702,11 @@ A `ServerSession` is created on the server to provide a resource. _Type_ ```ts -abstract class Session extends Supervisor.t { +abstract class Session extends Supervisor.t { readonly injector?: Injector.t; - readonly input: Observable.Observer>; - readonly output: Observable.t>; - readonly protocol: Subprotocol; + readonly input: Observable.Observer>; + readonly output: Observable.t>; + readonly protocol: Subprotocol; } ``` @@ -1748,9 +1748,9 @@ const resource = Session.Resource(); _Constructor_ ```ts -function client( - options: ClientOptions -): ClientSession; +function client( + options: ClientOptions +): ClientSession; ``` Creates a new `ClientSession`. @@ -1766,8 +1766,8 @@ import type { Api } from "..."; const httpProtocol = Subprotocol.init({ connectionMode: Subprotocol.ConnectionMode.ConnectionLess, + dataType: Subprotocol.DataType(), operationMode: Subprotocol.OperationMode.Unicast, - protocol: Subprotocol.Protocol(), transmissionMode: Subprotocol.TransmissionMode.HalfDuplex }); @@ -1784,9 +1784,9 @@ const client = session.createProxy(); _Constructor_ ```ts -function server( - options: ServerOptions -): ServerSession; +function server( + options: ServerOptions +): ServerSession; ``` Creates a new `ServerSession`. @@ -1804,8 +1804,8 @@ module User { const httpProtocol = Subprotocol.init({ connectionMode: Subprotocol.ConnectionMode.ConnectionLess, + dataType: Subprotocol.DataType(), operationMode: Subprotocol.OperationMode.Unicast, - protocol: Subprotocol.Protocol(), transmissionMode: Subprotocol.TransmissionMode.HalfDuplex }); @@ -1934,7 +1934,7 @@ Subscribes to state changes and values emitted by the subject. _Module_ -The Transporter protocol is type agnostic. In order to provide type-safety a subprotocol is required. The subprotocol restricts what types may be included in function IO. For example, if the subprotocol is JSON then only JSON data types may be input or output from remote functions. +The Transporter protocol is type agnostic. In order to provide type-safety a subprotocol is required. The subprotocol restricts what types may be included in function IO. For example, if the data type of a subprotocol is JSON then only JSON data types may be input or output from remote functions. In addition, Transporter can perform recursive RPC if certain subprotocol and network conditions are met. Recursive RPC means functions or proxies may be included in function IO. This is an interesting concept because it allows state between processes to be held on the call stack. For example, recursive RPC allows Observables to be used for pub-sub. @@ -1943,14 +1943,14 @@ In order to use recursive RPC your subprotocol must be connection-oriented and b ###### Types - [ConnectionMode](#ConnectionMode) +- [DataType](#Subprotocol_DataType) - [OperationMode](#OperationMode) -- [Protocol](#Subprotocol_Protocol) - [Subprotocol](#Subprotocol_Subprotocol) - [TransmissionMode](#TransmissionMode) ###### Constructors -- [Protocol](#Subprotocol_Protocol_2) +- [DataType](#Subprotocol_DataType_2) - [init](#Subprotocol_Init) ###### Functions @@ -1978,6 +1978,16 @@ enum ConnectionMode { Used to define the type of connection between the client and the server. +

DataType

+ +_Type_ + +```ts +interface DataType {} +``` + +Constrains the input and output types of a procedure to a specific data type. + #### OperationMode _Type_ @@ -2004,22 +2014,12 @@ enum OperationMode { Used to define how data is distributed to nodes in a network. -

Protocol

- -_Type_ - -```ts -interface Protocol {} -``` - -A container type for a subprotocol. This is necessary since TypeScript lacks partial inference of type parameters. -

Subprotocol

_Type_ ```ts -interface Subprotocol {} +interface Subprotocol {} ``` Used to restrict function input and output types as well as determine if recursive RPC can be enabled or not. @@ -2047,23 +2047,23 @@ enum TransmissionMode { Used to define how data is transmitted over a network. -

Protocol

+

DataType

_Constructor_ ```ts -function Protocol(): Protocol; +function DataType(): DataType; ``` -Creates a new `Protocol`. +Creates a new `DataType`. ##### Example ```ts -import * as Subprotocol from "@daniel-nagy/transporter/Subprotocol"; import * as Json from "@daniel-nagy/transporter/Json"; +import * as Subprotocol from "@daniel-nagy/transporter/Subprotocol"; -const jsonProtocol = Subprotocol.Protocol(); +const jsonDataType = Subprotocol.DataType(); ```

Init

@@ -2073,8 +2073,8 @@ const jsonProtocol = Subprotocol.Protocol(); ```ts function init(options: { connectionMode: ConnectionMode; + dataType: DataType; operationMode: OperationMode; - protocol: Protocol; transmissionMode: TransmissionMode; }): Subprotocol; ``` @@ -2089,8 +2089,8 @@ import * as SuperJson from "@daniel-nagy/transporter/SuperJson"; const subprotocol = Subprotocol.init({ connectionMode: Session.ConnectionMode.ConnectionLess, + dataType: Subprotocol.DataType(), operationMode: Session.OperationMode.Unicast, - protocol: Subprotocol.Protocol(), transmissionMode: Session.TransmissionMode.HalfDuplex }); ``` @@ -2113,8 +2113,8 @@ import * as SuperJson from "@daniel-nagy/transporter/SuperJson"; const subprotocol = Subprotocol.init({ connectionMode: Session.ConnectionMode.ConnectionLess, + dataType: Subprotocol.DataType(), operationMode: Session.OperationMode.Unicast, - protocol: Subprotocol.Protocol(), transmissionMode: Session.TransmissionMode.HalfDuplex }); @@ -2125,7 +2125,7 @@ Subprotocol.isBidirectional(subprotocol); // true _Module_ -The SuperJson module extends the JSON protocol to include many built-in JavaScript types, including `Date`, `RegExp`, `Map`, ect. +The SuperJson module extends JSON to include many built-in JavaScript types, including `Date`, `RegExp`, `Map`, ect. ###### Types diff --git a/packages/core/src/Session.test.ts b/packages/core/src/Session.test.ts index 964a58c..3a76a15 100644 --- a/packages/core/src/Session.test.ts +++ b/packages/core/src/Session.test.ts @@ -369,8 +369,8 @@ test("proxies are referentially stable", () => { test("unidirectional protocols do not return a value", async () => { const protocol = Subprotocol.init({ connectionMode: Subprotocol.ConnectionMode.Connectionless, + dataType: Subprotocol.DataType(), operationMode: Subprotocol.OperationMode.Broadcast, - protocol: Subprotocol.Protocol(), transmissionMode: Subprotocol.TransmissionMode.Simplex }); @@ -397,8 +397,8 @@ test("unidirectional protocols do not return a value", async () => { const jsonProtocol = Subprotocol.init({ connectionMode: Subprotocol.ConnectionMode.ConnectionOriented, + dataType: Subprotocol.DataType(), operationMode: Subprotocol.OperationMode.Unicast, - protocol: Subprotocol.Protocol(), transmissionMode: Subprotocol.TransmissionMode.Duplex }); diff --git a/packages/core/src/Session.ts b/packages/core/src/Session.ts index af18a90..9838ece 100644 --- a/packages/core/src/Session.ts +++ b/packages/core/src/Session.ts @@ -53,20 +53,20 @@ export const rootSupervisor = Supervisor.init("RootSupervisor"); * automatically terminated. */ export abstract class Session< - Protocol = unknown, + DataType = unknown, Value = unknown > extends Supervisor.t { - #input = Subject.init>(); - #output = Subject.init>(); + #input = Subject.init>(); + #output = Subject.init>(); public readonly input = this.#input as Required< - Observable.Observer> + Observable.Observer> >; public readonly output = this.#output.asObservable(); constructor( - public readonly protocol: Subprotocol, + public readonly protocol: Subprotocol, public readonly injector?: Injector.t ) { super(); @@ -211,12 +211,12 @@ export abstract class Session< * Represents a client session. A proxy can be created from a client session for * calling remote functions. */ -export class ClientSession extends Session { +export class ClientSession extends Session { #clientAgent: ClientAgent.t; constructor( public readonly injector: Injector.t | undefined, - public readonly protocol: Subprotocol + public readonly protocol: Subprotocol ) { super(protocol, injector); this.#clientAgent = this.createClient(ROOT_AGENT_ADDRESS); @@ -233,10 +233,10 @@ export class ClientSession extends Session { /** * Represents a server session. */ -export class ServerSession extends Session { +export class ServerSession extends Session { constructor( public readonly injector: Injector.t | undefined, - public readonly protocol: Subprotocol, + public readonly protocol: Subprotocol, public readonly value: Value ) { super(protocol, injector); @@ -261,30 +261,30 @@ export const Resource = (): Resource => ({ * The transporter protocol is type agnostic. Subprotocols are required for * type safety. */ -type Subprotocol = Subprotocol.t< - Protocol, +type Subprotocol = Subprotocol.t< + DataType, JsFunction.Input>, JsFunction.Output> >; -interface Options { +interface Options { injector?: Injector.t; - protocol: Subprotocol; + protocol: Subprotocol; } -export interface ClientOptions - extends Options { +export interface ClientOptions + extends Options { resource: Resource; } /** * Creates a new `ClientSession`. */ -export function client({ +export function client({ injector, protocol, resource: _ -}: ClientOptions): ClientSession { +}: ClientOptions): ClientSession { return new ClientSession(injector, protocol); } @@ -296,11 +296,11 @@ export interface ServerOptions /** * Creates a new `ServerSession`. */ -export function server({ +export function server({ injector, protocol, provide: value -}: ServerOptions): ServerSession { +}: ServerOptions): ServerSession { return new ServerSession(injector, protocol, value); } diff --git a/packages/core/src/Session.typetest.ts b/packages/core/src/Session.typetest.ts index dadb6d8..f200c18 100644 --- a/packages/core/src/Session.typetest.ts +++ b/packages/core/src/Session.typetest.ts @@ -7,8 +7,8 @@ import * as Subprotocol from "./Subprotocol.js"; describe("recursive protocols", () => { const protocol = Subprotocol.init({ connectionMode: Subprotocol.ConnectionMode.ConnectionOriented, + dataType: Subprotocol.DataType(), operationMode: Subprotocol.OperationMode.Unicast, - protocol: Subprotocol.Protocol(), transmissionMode: Subprotocol.TransmissionMode.Duplex }); @@ -562,8 +562,8 @@ describe("recursive protocols", () => { describe("unidirectional protocols", () => { const protocol = Subprotocol.init({ connectionMode: Subprotocol.ConnectionMode.Connectionless, + dataType: Subprotocol.DataType(), operationMode: Subprotocol.OperationMode.Broadcast, - protocol: Subprotocol.Protocol(), transmissionMode: Subprotocol.TransmissionMode.Simplex }); diff --git a/packages/core/src/Subprotocol.ts b/packages/core/src/Subprotocol.ts index 88830a2..caaa4af 100644 --- a/packages/core/src/Subprotocol.ts +++ b/packages/core/src/Subprotocol.ts @@ -56,16 +56,17 @@ export enum TransmissionMode { } // eslint-disable-next-line @typescript-eslint/no-unused-vars -export interface Protocol {} +export interface DataType {} /** - * A container type for a subprotocol. This exists as a workaround to the - * absence of partial type inference in TypeScript. + * Constrains the input and output types of a procedure to a specific data type. + * A container type is necessary due to the absence of partial type inference in + * TypeScript. */ -export const Protocol = (): Protocol => ({}); +export const DataType = (): DataType => ({}); // eslint-disable-next-line @typescript-eslint/no-unused-vars -interface Subprotocol { +interface Subprotocol { connectionMode: ConnectionMode; operationMode: OperationMode; transmissionMode: TransmissionMode; @@ -76,8 +77,8 @@ export type { Subprotocol as t }; /** * The Transporter protocol is type agnostic. In order to provide type-safety a * subprotocol is required. The subprotocol restricts what types may be included - * in function IO. For example, if the subprotocol is JSON then only JSON data - * types may be input or output from remote functions. + * in function IO. For example, if the data type of a subprotocol is JSON then + * only JSON data types may be input or output from remote functions. * * In addition, Transporter can perform recursive RPC if certain subprotocol and * network conditions are met. Recursive RPC means functions or proxies may be @@ -92,30 +93,30 @@ export type { Subprotocol as t }; * application of Transporter in the browser especially interesting. */ const Subprotocol = < - const SubProtocol, + const Type, const Connection extends ConnectionMode, const Operation extends OperationMode, const Transmission extends TransmissionMode >({ connectionMode, + dataType: _dataType, operationMode, - protocol: _protocol, transmissionMode }: { connectionMode: Connection; + dataType: DataType; operationMode: Operation; - protocol: Protocol; transmissionMode: Transmission; }): Subprotocol< - SubProtocol, - Io, + Type, + Io, "yes" extends Bidirectional - ? Promise> + ? Promise> : void > => ({ connectionMode, operationMode, transmissionMode }); /** - * Creates a new `SubProtocol`. + * Creates a new `Subprotocol`. */ export const init = Subprotocol;