Skip to content

Commit

Permalink
✨Add implementation for JavaScript + Effection
Browse files Browse the repository at this point in the history
- Deno
- Node
  • Loading branch information
cowboyd committed Aug 30, 2023
1 parent d0f5ff1 commit 49ea74c
Show file tree
Hide file tree
Showing 8 changed files with 274 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,4 @@ docker run -it -p8080:8080 ghcr.io/jamesward/easyracer --debug
| Elm | [elm-worker](elm-worker) | ![tests](https://github.com/jamesward/easyracer/actions/workflows/elm-worker.yaml/badge.svg) | [Jack Leow](https://github.com/jackgene) | |
| Rust + Tokio | [rust-tokio](rust-tokio) | ![tests](https://github.com/jamesward/easyracer/actions/workflows/rust-tokio.yaml/badge.svg) | [James Ward](https://github.com/jamesward) and Rust Developer Retreat Participants | |
| JavaScript | [javascript-stdlib](javascript-stdlib) | ![tests](https://github.com/jamesward/easyracer/actions/workflows/javascript-stdlib.yaml/badge.svg) | [James Ward](https://github.com/jamesward) | |
| JavaScript + Effection | [javascript-effection](javascript-effection)| | | [Charles Lowell](https://github.com/cowboyd) | Scenario 3 currently timing out|
3 changes: 3 additions & 0 deletions javascript-effection/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
deno.lock
/node_modules
package-lock.json
24 changes: 24 additions & 0 deletions javascript-effection/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
## EasyRacer: Javascript + Effection

This implementation uses [Effection v3](https://effection.deno.dev)

In order to run the tests, you will first need to run the scenario server, so
see the [main readme](../README.md) for how to do that.

### Deno

make sure you have [Deno](https://deno.land/x) installed.

```shellsession
$ deno task test
```

### Node

You will need [Node](https://nodejs.org) >= 18 installed. If you are using,
[Volta](https://volta.sh) this will be configured for you automatically.

``` shellsession
$ npm install
$ npx tsx --test easyracer.test.ts
```
16 changes: 16 additions & 0 deletions javascript-effection/deno.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"tasks": {
"test:deno": "deno test --allow-net --v8-flags=--max-old-space-size=8192",
"test:node": "npx tsx --test *.test.ts"
},
"lint": {
"rules": {
"exclude": ["prefer-const", "require-yield"]
}
},
"imports": {
"effection": "https://deno.land/x/[email protected]/mod.ts",
"expect": "https://deno.land/x/[email protected]/mod.ts",
"node:test": "https://deno.land/[email protected]/testing/bdd.ts"
}
}
45 changes: 45 additions & 0 deletions javascript-effection/easyracer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { describe, scenario } from "./test-helpers.ts";
import { spawn } from "effection";
import { delay, request, timeout } from "./easyracer.ts";

describe("easyracer", () => {
scenario("1", () => [request(), request()]);
scenario("2", () => [request(), request()]);
scenario.skip("3", () => Array(10_000).fill(request()));
scenario("4", () => [request(), timeout(1000, request())]);
scenario("5", () => [request(), request()]);
scenario("6", () => [request(), request(), request()]);
scenario("7", () => [request(), delay(3000, request())]);
scenario("8", () => {
function* res() {
let id = yield* request("open");
try {
return yield* request(`use=${id}`);
} finally {
yield* request(`close=${id}`);
}
}

return [res(), res()];
});

scenario("9", function* () {
let answer = "";
let tasks = [];
for (let i = 0; i < 10; i++) {
let task = yield* spawn(function* () {
let result = yield* request();
if (result.length === 1) {
answer += result;
}
});
tasks.push(task);
}

for (let task of tasks) {
yield* task;
}

return answer;
});
});
92 changes: 92 additions & 0 deletions javascript-effection/easyracer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import {
action,
call,
createContext,
expect as $await,
type Operation,
sleep,
spawn,
type Task,
useAbortSignal,
} from "effection";

export const BaseURL = createContext<string>(
"BaseURL",
"http://localhost:8080",
);
export const Scenario = createContext<string>("Scenario");

export function* delay<T>(millis: number, op: Operation<T>): Operation<T> {
yield* sleep(millis);
return yield* op;
}

/**
* returns either the result of `op`, or a timeout string after `limit` ms
*/
export function timeout<T>(
limit: number,
op: Operation<T>,
): Operation<T | string> {
return action(function* (resolve) {
yield* spawn(function* () {
yield* sleep(limit);
resolve(`timeout of ${limit}ms exceeded`);
});

resolve(yield* op);
});
}

/**
* Execute a request corresponding to the current scenario.
*/
export const request = (query?: string) =>
call(function* () {
let signal = yield* useAbortSignal();
let url = `${yield* BaseURL}/${yield* Scenario}${query ? "?" + query : ""}`;

let request = fetch(url, { signal });

try {
let response = yield* $await(request);
if (response.ok) {
let text = yield* $await(response.text());
return text;
} else {
return `${response.status}: ${response.statusText}`;
}
} catch (error) {
return String(error);
}
});

/**
* return the result of the first operation whose response is "right". Or the
* last result that wasn't "right";
*/
export function rightOrNot(ops: Operation<string>[]): Operation<string> {
return action(function* (resolve) {
let tasks: Task<string>[] = [];
for (let operation of ops) {
let task = yield* spawn(function* () {
let result = yield* operation;
if (result === "right") {
// right!
resolve("right");
}
return result;
});
tasks.push(task);
}
let last: string | null = null;
for (let task of tasks) {
let result = yield* task;
if (result !== "right") {
last = result;
}
}
// not!
resolve(String(last));
});
}
13 changes: 13 additions & 0 deletions javascript-effection/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "easyracer",
"version": "0.0.0",
"private": true,
"type": "module",
"dependencies": {
"effection": "3.0.0-alpha.13",
"expect": "*"
},
"volta": {
"node": "20.5.1"
}
}
80 changes: 80 additions & 0 deletions javascript-effection/test-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import {
afterEach,
beforeEach,
describe as $describe,
it,
} from "node:test";
import { expect } from "expect";
import {
action,
createScope,
type Operation,
spawn,
type Task,
} from "effection";

import { Scenario } from "./easyracer.ts";

let [scope, destroy] = createScope();

export function describe(name: string, fn: () => void): void {
$describe(name, () => {
beforeEach(() => {
[scope, destroy] = createScope();
});

afterEach(destroy);

fn();
});
}

type ScenarioFn = () => Operation<unknown> | Operation<unknown>[];

export function scenario(number: string, fn: ScenarioFn): void {
it(`scenario ${number}`, runScenario(number, fn));
}

scenario.only = (number: string, _op: ScenarioFn) => {
it.only(`scenario ${number}`, () => {});
};

scenario.skip = (number: string, _op: ScenarioFn) => {
it.skip(`scenario ${number}`, () => {});
};

export function rightOrNot(ops: Operation<unknown>[]): Operation<string> {
return action(function* (resolve) {
let tasks: Task<unknown>[] = [];
for (let operation of ops) {
let task = yield* spawn(function* () {
let result = yield* operation;
if (result === "right") {
// right!
resolve("right");
}
return result;
});
tasks.push(task);
}
let last: unknown = null;
for (let task of tasks) {
last = yield* task;
}
// not!
resolve(String(last));
});
}

function runScenario(number: string, fn: ScenarioFn) {
return () =>
scope.run(function* () {
yield* Scenario.set(number);
let scenario = fn();
let ops = Array.isArray(scenario) ? scenario : [scenario];

let result = yield* rightOrNot(ops);

expect(result).toEqual("right");
});
}

0 comments on commit 49ea74c

Please sign in to comment.