Skip to content

Commit

Permalink
Add remote package for calling interactors on a remote process
Browse files Browse the repository at this point in the history
  • Loading branch information
jnicklas committed Jan 29, 2022
1 parent fe303e0 commit bcb5286
Show file tree
Hide file tree
Showing 9 changed files with 404 additions and 5 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"packages/globals",
"packages/core",
"packages/keyboard",
"packages/remote",
"packages/html",
"packages/material-ui",
"packages/with-cypress"
Expand Down
12 changes: 12 additions & 0 deletions packages/remote/example/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { connect } from '../src/index';
import { main } from 'effection';
import { Heading, Link } from '@interactors/html';

main(function*() {
console.log("[client] connecting...");
yield connect('ws://127.0.0.1:30400');
console.log("[client] connected!");

yield Heading('Hello World').exists();
yield Link('Some Link').has({ href: '/incorrect' });
});
20 changes: 20 additions & 0 deletions packages/remote/example/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { serve } from '../src/index';
import { main } from 'effection';
import { Heading, Link } from '@interactors/html';
import { JSDOM } from 'jsdom';
import { setDocumentResolver } from '@interactors/globals';

let dom = new JSDOM(`
<!doctype html>
<html>
<h1>Hello World</h1>
<a href="/foobar">Some Link</a>
</html>
`);

setDocumentResolver(() => dom.window.document);

main(function*() {
console.log('[server] starting');
yield serve(30400, { heading: Heading, link: Link });
});
60 changes: 60 additions & 0 deletions packages/remote/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
"name": "@interactors/remote",
"version": "1.0.0-rc1.2",
"description": "Call interactors running in a remote process",
"main": "dist/cjs/index.js",
"browser": "dist/esm/index.js",
"types": "dist/index.d.ts",
"repository": "https://github.com/thefrontside/interactors.git",
"homepage": "https://frontside.com/interactors",
"author": "Frontside Engineering <engineering@frontside.com>",
"license": "MIT",
"files": [
"dist/**/*",
"src/**/*",
"README.md"
],
"exports": {
".": {
"require": "./dist/cjs/index.js",
"default": "./dist/esm/index.js"
}
},
"scripts": {
"clean": "rm -rf dist *.tsbuildinfo",
"lint": "eslint \"{src,test}/**/*.ts\"",
"check:types": "tsc --noEmit",
"test": "mocha -r ts-node/register \"test/**/*.test.ts\"",
"docs": "rm -rf docs && yarn typedoc --options typedoc.json",
"docs:netlify": "yarn prepack && yarn docs",
"docs:preview": "yarn parcel docs/api/v1/index.html",
"prepack": "tsc --build ./tsconfig.build.json && yarn prepack:es2015 && yarn prepack:commonjs",
"prepack:es2015": "tsc --project ./tsconfig.build.json --outdir dist/esm --module es2015",
"prepack:commonjs": "tsc --project ./tsconfig.build.json --outdir dist/cjs --module commonjs"
},
"dependencies": {
"@effection/dispatch": "^2.0.3",
"@effection/websocket-client": "^2.0.3",
"@effection/websocket-server": "^2.0.3",
"@interactors/core": "1.0.0-rc1.2",
"@interactors/globals": "1.0.0-rc1.1",
"uuid": "^8.3.2"
},
"devDependencies": {
"@frontside/tsconfig": "^1.2.0",
"@types/mocha": "^7.0.1",
"@types/node": "^14.17.5",
"@types/uuid": "^8.3.4",
"expect": "^24.9.0",
"jsdom": "^16.2.2",
"mocha": "^6.2.2",
"parcel": "^2.0.0-beta.2",
"ts-node": "^10.4.0",
"typedoc": "^0.22.7",
"typescript": "~4.4.4"
},
"volta": {
"node": "14.17.5",
"yarn": "1.22.11"
}
}
98 changes: 98 additions & 0 deletions packages/remote/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Operation, spawn } from 'effection';
import { addInteractionWrapper } from '@interactors/globals';
import { InteractorConstructor, Interactor } from '@interactors/core';
import { createWebSocketServer, WebSocketConnection, WebSocketServer } from '@effection/websocket-server';
import { createWebSocketClient, WebSocketClient } from '@effection/websocket-client';
import { createDispatch } from '@effection/dispatch';
import { v4 as uuid } from 'uuid';

type ServerMessage = {
key: string;
method: string;
args: unknown[];
options: SerializedOptions,
};

type ClientMessage = {
key: string;
status: "success" | "error";
value?: unknown;
error?: string;
};

// TODO: we should change InteractionOptions in core so it is more easily serializable
type SerializedOptions = {
name: string;
locator: string | undefined;
filters: Record<string, unknown>;
ancestors: SerializedOptions[];
}

function serializeOptions(options: Interactor<any, any>['options']): SerializedOptions {
return {
name: options.name,
locator: options.locator?.value as string | undefined,
filters: options.filter.filters,
ancestors: options.ancestors.map(serializeOptions),
}
}

export function connect(url: string): Operation<void> {
return {
*init() {
let client: WebSocketClient<ClientMessage, ServerMessage> = yield createWebSocketClient(url);
let dispatch = createDispatch();

addInteractionWrapper(function*(interaction) {
let key = uuid();
let interactor = interaction.interactor as Interactor<any, any>;
yield client.send({
key,
method: interaction.method,
args: interaction.args,
options: serializeOptions(interactor.options),
});

let reply = yield dispatch.get(key).expect();
if(reply.status === 'error') {
throw new Error(reply.error || "unknown error");
} else {
return reply.value;
}
})

yield spawn(client.forEach(function*(message) {
dispatch.send(message.key, message);
}));
}
}
}

export function* serve(port: number, map: Record<string, InteractorConstructor<any, any, any, any>>): Operation<void> {
console.log('[server] strarting...');
let server: WebSocketServer<ServerMessage, ClientMessage> = yield createWebSocketServer({ port });

console.log(`[server] started! Listening on port ${port}`);

while(true) {
let connection: WebSocketConnection<ServerMessage, ClientMessage> = yield server.first();
if(!connection) break;

yield spawn(connection.forEach(({ options, key, method, args }) => function*() {
console.log('[server] received request', key, options);
let interactor = options.locator ?
map[options.name](options.locator, options.filters) :
map[options.name](options.filters);

try {
let value = yield interactor[method](...args);
console.log('[server] success', key);
yield connection.send({ key, status: "success", value });
} catch(error: any) {
console.log('[server] error', key);
yield connection.send({ key, status: "error", error: error.message });
}
}));
}
}
15 changes: 15 additions & 0 deletions packages/remote/tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"extends": "../../tsconfig-base.json",
"compilerOptions": {
"outDir": "dist/cjs",
"rootDir": "./src",
"declarationDir": "dist"
},
"include": [
"src/**/*.ts"
],
"references": [
{ "path": "../core/tsconfig.build.json" },
{ "path": "../globals/tsconfig.build.json" }
]
}
4 changes: 4 additions & 0 deletions packages/remote/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "@frontside/tsconfig",
"exclude": ["types/**/*.ts"]
}
7 changes: 7 additions & 0 deletions packages/remote/typedoc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"out": "docs/api/v1",
"name": "@interactors/html",
"entryPoints": "src/index.ts",
"includeVersion": true,
"excludePrivate": true
}
Loading

0 comments on commit bcb5286

Please sign in to comment.