A TypeScript-based effects system for managing and recording side effects in applications, with support for transaction recording and playback.
If you found yourself here, you probably already know that effects are anything other than pure computations over values. Some examples would be HTTP requests, reading from or writing to disk, etc.
This project implements an effects system that allows you to:
- Execute and manage side effects in a controlled manner
- Record transactions and their results for later playback
- Compare live executions with recorded transactions
- Interactive decision making for handling mismatches between recorded and live effects
What does that actually mean? Check it out...
First, start the server
npm run start
Then, run the test script with no arguments
npx tsx ./test.ts
Nothing happened.
Yes, exactly. The system is based on futures, which are kind of like promises, but better. (You can read about some of the reasons why promises are broken).
What happened behind the scenes is that a fully-specified computation graph of multiple HTTP transactions and data transformations was constructed. This is cool because it means you can analyze the state and behavior of a system at runtime, but without actually touching anything (i.e. hitting APIs, etc).
Next, try running it in --inspect
mode
You'll see a top-level overview of the computation to be run:
Future {
_spawn: Future {
_fn: [Function (anonymous)],
_a: {
method: 'post',
url: 'http://localhost:1138/token',
body: { username: 'test@account', password: 's3krit' }
},
context: <ref *1> { head: null, tail: [Circular *1] }
},
// ...
This is just native console.log()
and util.inspect()
, but with a real inspector and a few class / function annotations, you could see details of exactly what will happen.
Actually running it
npx tsx ./test.ts --run
Predictably, this executes the compute graph, and returns the results of the sequence of API calls:
OK
[
{ name: 'Acme, Inc.', ticker: 'ACM', quantity: 5, price: 1.11 },
{ name: 'Macrosaft', ticker: 'MASF', quantity: 11, price: 25.01 },
{
name: 'Goodnight Moonshot',
ticker: 'GMI',
quantity: 5000,
price: 0.22
},
{ name: 'BearBNB', ticker: 'BBNB', quantity: 40, price: 15.28 }
]
We should also see some output from the server:
POST /token
GET /accounts
GET /accounts/1
GET /accounts/2
GET /accounts/2/holdings
Recording mode
Let's run it again, but with the --out
flag:
npx tsx ./test.ts --run --out tx.json
Same thing happens, but now, there's a tx.json
file in the directory, that contains a record of inputs and outputs of all the effects that were run.
Playback mode
To see the transcript in action, swap --out
for --in
:
npx tsx ./test.ts --run --in tx.json
You'll notice we get the same result, but no additional logs appear on the server. The system is matching up requests from the transcript and replaying the responses. This is already pretty handy for iteratively working on apps that hit an API and not having to hit the API all the time.
Playback with changes
Now try changing test.ts
to call a different initial endpoint:
/**
* Try swapping these two lines after recording a run and then re-running.
*/
- getWithAuth(authToken)(`${API_ROOT}/v2/accounts`)
- // getWithAuth(authToken)(`${API_ROOT}/accounts`)
+ // getWithAuth(authToken)(`${API_ROOT}/v2/accounts`)
+ getWithAuth(authToken)(`${API_ROOT}/accounts`)
.chain(pipe(
Re-running it with --in tx.json
yields the following:
Executed command `Get` differs from transcript:
{
"type" : "Http.Get",
"data" : {
"method" : "get",
"url" : "http://localhost:1138/v2/accounts",
"headers" : {
"authToken" : "THE-S3KRIT-T0K3N"
}
}
}
Execute live effect?
It noticed that the script is trying to run one of the effects with different parameters than the transcript, and prompts to execute it live or replay from the transcript anyway. The diff view in the prompt shows what's changed.
Type y
to run it live. As the server log shows, it only re-runs effects where the inputs have changed.
These features alone, the ability to record and playback transcripts of side effects, enable a number of interesting features:
- Offline testing & development of systems that interact with remote APIs
- Selectively isolating subsystems within an application
- Diffing inputs and outputs to external systems between runs using
--in
and--out
together - Capturing detailed traces in remote applications, live
These few features allow powerful insight into and control over applications, even in scenarios where tools like a Chrome inspector session are unavailable.
Imagine taking it further with interactive terminal sessions: check out http://localhost:1138/interactive—when you visit, the server terminal session prompts you for input:
Test service running at http://localhost:1138
Test value: _
Whatever you type is then echoed in the HTTP response:
{
"result": "We'll do it live!"
}
- Abstract
Message
class for representing effects - Type-safe message handling with TypeScript generics
- JSON serialization support for messages
- Record effect executions and their results
- Save transaction logs to files
- Replay recorded transactions
- Interactive diff viewing when live effects don't match recorded ones
- CLI-based interactive mode for handling effect discrepancies
- Diff visualization for comparing expected vs actual effects
- Option to execute live effects or use recorded results
npm install
-
Runtime Dependencies
axios
: HTTP client for making API requestsexpress
: Web server frameworkfluture
: Functional future implementation for handling async operationsramda
: Functional programming utilitiesdifflet
: For generating readable diffsyargs
: Command-line argument parsing
-
Development Dependencies
typescript
: TypeScript compiler and type definitions
The system is built around several key components:
- Message System: Base classes for defining effects
- Transaction Recording: Infrastructure for recording and replaying effects
- Interactive Terminal: UI for handling discrepancies between recorded and live effects
- Future-based Effects: Uses the Fluture library for handling asynchronous operations
BSD-3