Coding soon

Coding soon

Unchain

A lightweight flexible asynchronous method chaining library to turn Javascript classes into fluent APIs

Hello Unchain

// sample async task
const wait = (ms = 200) => 
	new Promise(later => 
		setTimeout(later, ms));
const Testing = Unchain.from(
	class Testing extends Unchain {

		async chained() {
			await wait();
			return "chained"; // not this
		}

		async asynchronous() {
			await wait();
			return "async"; // not this
		}

		async methods() {
			await wait();
			return "methods"; // just return
		}

		async and() {
			await wait();
			return "and"; // meaningful values
		}

	}
);
new Testing().chained()
.asynchronous().methods()
.and().then(console.warn);

run
clear
Test

Basic Usage

Extend Unchain class

class ChainClass extends Unchain {
	
	hello() {
		return "Hello";
	}
	
	world() {
		return "World";
	}

	greet(who) {
		return "Hi " + who + "!";
	}

}

Call Unchain.from method

const Chain = Unchain.from(ChainClass);

Run the chain

new Chain().hello().world()
.then(console.log);

run
clear
HelloUnchain

Promise chain

Unchain class behaves like any promise and makes no difference between sync and async methods.

Write class with sync and async methods

const Chaining = Unchain.from(
	class Chaining extends Unchain {

		synchronous() {
			return "synchronous";
		}

		and() {
			return "and";
		}

		async asynchronous() {
			await wait();
			return "asynchronous";
		}
		
		async methods() {
			await wait();
			return "methods";
		}

	}
);

Run the chain

new Chaining()
.synchronous().and()
.asynchronous().methods()
.then(console.log);

run
clear
ChainingThen

Or await

console.log(
	await new Chaining()
	.synchronous().and()
	.asynchronous().methods()
);
Error: Snippet(s) not found: SomeChain

Clear syntax

No more return this;

noMoreThis() {
	return {any: "value"};
}

Create chain without new keyword

Chain().hello().world()
.then(console.log);

run
clear
NoNew

Call methods without parentheses if not passing arguments

Chain.hello.world
.then(console.log);

run
clear
NoParentheses

Chaining.synchronous.and
.asynchronous.methods
.then(console.log);
Error: Snippet(s) not found: SomeChain

Use parentheses when passing arguments

Chain.greet("everyone")
.then(console.log);

run
clear
Parentheses

Error handling

As usual:

  • .then(fullfilled, rejected)
  • .catch
  • try {} catch(err) {}

Small tweak: .finally receives chain instance.

const Unstable = Unchain.from(
	class Unstable extends Unchain {

		test() {
			return "test";
		}

		async ohNo() {
			await wait();
			throw new Error("oh no !");
		}

	}
);

Throwing error from chained method

Unstable.test.ohNo
.then(console.log, console.error)
.finally(failChain => // chain instance
	console.log("before fail:", failChain.val)
);

run
clear
UnstableChain

Unstable.test.ohNo
.then(console.log)
.catch(console.error)
.finally(failChain => 
	console.log(failChain.val)
);

run
clear
TryCatch

try {
	console.log(await Unstable.test.ohNo);
}
catch(err) {
	console.error(err);
}

run
clear
TryCatch

Method does not exist

Unstable.test.oops
.then(console.log)
.catch(console.error)
.finally(failChain => 
	console.log(failChain.val)
);
Error: Snippet(s) not found: SomeChain
try {
	console.log(await Unstable.test.oops);
}
catch(err) {
	console.error(err);
}
Error: Snippet(s) not found: SomeChain

Chain data

Unchain provides two storage mechanisms.

  • this.val: chained methods return values storage array
  • this.dat: store and share data during chain execution
  • this.cur: previous chained call return value

Constructor accepts two optional arguments to set dat and/or val initial values.
Use it to inject data before chain execution, create multiple chains with identical data, or recover chain state after a crash.

class Counter extends Unchain {

	constructor() {
		super({count: 0}); // set initial this.dat
		// dat, or val, or both, in any order
		// super(["+3"], {count: 3});
	}
	
	increment(amount = 1) {
		this.dat.count += amount;
		return `+${amount}`;
	}
	
	current() {
		return `current count: ${this.dat.count}`;
	}

}

const Count = Unchain.from(Counter);
Count
.increment // default +1
.increment(2)
.increment(3)
.current.then(console.log);

run
clear
Data

Output Modes

Call .mode("____") to set final promise return type.

const Handler = Unchain.from(
	class DataHandler extends Unchain {

		constructor() {
			super({items: []});
		}
		
		add(item) {
			this.dat.items.push(item);
			return `added ${item}`;
		}

	}
);

"vals": default, output methods return values array

Handler //.mode("vals")
.add("one").add("two")
.then(console.log);

run
clear
OutputVals

"data": output chain data object

Handler.mode("data")
.add("one").add("two")
.then(console.log);

run
clear
OutputData

"last": output last chained call return value

Handler.mode("last")
.add("one").add("two")
.then(console.log);

run
clear
OutputLast

"this": output chain instance

Handler.mode("this")
.add("one").add("two")
.then(chain => 
	console.log(chain.val, chain.dat));

run
clear
OutputThis

Extending chains

Build class inheritance tree as usual.

// Simple steps runner
class StepsChain extends Unchain {

	async step1() {
		await wait();
		return "step 1";
	}

	async step2() {
		await wait();
		return "step 2";
	}
	
}

// Not needed, will run from subclasses
// const Steps = Unchain.from(StepsChain);
// Extended steps runner
class MoreStepsChain extends StepsChain {
	
	async step3() {
		await wait();
		return "step 3";
	}

	async step4() {
		await wait();
		return "step 4";
	}
	
}

const MoreSteps = Unchain.from(MoreStepsChain);
MoreSteps
.step1.step2 // from super
.step3.step4 // from self 
.then(console.warn);

run
clear
Extends

External methods

Call .ext() to run any method without breaking chain execution.
Return value will be inserted into output array this.val.

MoreSteps
.step1.step2
.ext(
	async stepsRunner => {
		console.log("checking progress");
		await wait(1234); // async check
		return stepsRunner.val.length + " steps completed"
	}
)
.step3.step4
.then(console.warn);

run
clear
Ext

Pause and resume

Call .hold.then to pause chain execution.
Call .free to resume chain.

MoreSteps.step1.step2 // steps 1 & 2
.hold.then( // pause after step2
	async steps => { // chain ref
		console.log("paused");
		await wait(1234); // delay
		console.log("resume");
		steps.free // resume chain
		.step3.step4 // steps 3 & 4
		.then(console.log);
	}
);

run
clear
Hold

Subchains

Call this.sub inside chained methods to create subchains.

class SubStepsChain extends MoreStepsChain {

	// oneTwo = step1 + step2
	oneTwo() {
		return this.sub
		.step1.step2;
	}

	// threeFour = step3 + step4
	threeFour() {
		return this.sub
		.step3.step4;
	}
	
}

const SubSteps = Unchain.from(SubStepsChain);

Note that this.sub calls are not awaited, async subchains are automatically handled.

SubSteps
.oneTwo // steps 1 & 2
.threeFour // steps 3 & 4
.then(console.log);

run
clear
Sub

Deep subchains

const AllSteps = Unchain.from(
	class AllStepsChain extends SubStepsChain {

		// oneTwoThreeFour = oneTwo + threeFour
		oneTwoThreeFour() {
			return this.sub
			.oneTwo.threeFour;
		}
		
	}
);
AllSteps.oneTwoThreeFour // 1 & 2 + 3 & 4
.then(console.log);

run
clear
Deep

Chain swapping

Call .swap(classRef, ...args) to swap chain instance during execution.
Overload arguments are passed to next chain constructor.

class ABChain extends Unchain {

	constructor(char) {
		super({char});
	}

	async char() {
		await wait();
		return this.dat.char;
	}

}

const ABC = Unchain.from(ABChain);

Start from A, then swap to B

ABC("A") // new A
.char // "A"
.swap(ABC, "B") // swap to new B
.char // "B"
.then(console.log);

run
clear
AB

Call .back to return to previous chain

ABC("A") // new A
.char // "A"
.swap(ABC, "B") // swap to new B
.char // "B"
.back // back to A
.char // "A"
.then(console.log);

run
clear
ABA

Call .swap with no arguments to swap to next chain

ABC("A") // new A
.char // "A"
.swap(ABC, "B") // swap to new B
.char // "B"
.back // back to A
.char // "A"
.swap // swap to B
.char // "B"
.then(console.log);

run
clear
ABAB

Create single-use disposable chain instances

ABC("A") // new A
.char // "A"
.swap(ABC, "B") // swap to new B
.ext(B => // run external method
	"B:" + B.uid) // internal id
.char // "B"
.back // back to A
.char // "A"
.swap(ABC, "B") // clear & new B
.ext(B => // run external method
	"B:" + B.uid) // changed uid
.char // "B"
.then(console.log);

run
clear
ABAnewB

Link multiple chains and call .swap or .back to navigate

ABC("A") // new A
.char // "A"
.swap(ABC, "B") // A -> new B
.char // "B"
.swap(ABC, "C") // B -> new C
.char // "C"
.back.back // <- <- jump to A
.char // "A"
.swap // -> swap to B
.char // "B"
.swap // -> swap to C
.char // "C"
.back.back // <- <- jump to A
.char // "A"
.swap.swap // -> -> jump to C
.char // "C"
.back // <- back to B
.char // "B"
.back // <- back to A
.char // "A"
.then(console.log);
// A B C A B C A C B A

run
clear
SwapJump

Subchain swapping inside methods

class ABCSubSwap extends ABChain {

	constructor(char) {
		super(char);
	}

	subSwapB() {
	
		return this.sub
		.char // self "A"
		.swap(ABCSub, "B") // -> swap new B
		.char // "B"
		.back // <- back to self A
		.char; // "A"
	
	}

}

const ABCSub = Unchain.from(ABCSubSwap);
ABCSub("A") // new A
.subSwapB // "A" -> new B, "B", A <- B, "A"
.then(console.log);

run
clear
ABCSub

Data sync

Call .sync("____") to set chain swap sync mode.

"full": default, sync this.val and this.dat

RGB // new RGB
.mode("data") // data mode
.swap(Red) // -> Red
.value(255) // max red
.back // RGB <-
.swap(Green) // -> Green
.value(127) // half green
.back // RGB <-
.swap(Blue) // -> Blue
.value(0) // no blue
.back // RGB <-
.then(console.log); // Orange

run
clear
RGB

"vals": sync this.val only, separate this.dat

Count.sync("vals") // sync this.val
.mode("this") // testing
.increment // this count +1 = 1
.swap(Count) // sync val
.increment // that count +1 = 1
.back // sync val
.increment // this count +1 = 2
.then(count => console.log(count.val, count.dat.count));

run
clear
ValsSync

"data": sync this.dat only, separate this.val

Count.sync("data") // sync this.dat
.mode("this") // testing
.increment // +1 = 1
.swap(Count) // sync dat, keep val
.increment // +1 from new instance = 2
.back // sync dat, keep val
.increment // +1 from first = 3
.then(count => console.log(count.val, count.dat.count));

run
clear
DataSync

"self": no sync, separate this.dat and this.val

Count.sync("self") // no sync
.mode("this") // testing
.increment // +1 = 1 not sharing
.swap(Count) // swap no sync
.increment // +1 = 1 not sharing
.back // swap no sync
.increment // +1 = 2 not sharing
.then(count => console.log(count.val, count.dat.count));

run
clear
SelfSync

Cheatsheet

  • Use Unchain.from(classRef) to enable chain features
  • Create chains without new keyword
  • Constructor dat and/or val init values (optional)
  • Chained methods should not return this;
  • Return meaningful values, pushed to this.val
  • Previous call return value stored in this.cur
  • Use parentheses only when passing arguments
  • Promise-based, use .then(), .catch(), .finally()
  • Call .mode() to choose chain output
  • Use this.dat to share data between methods or chains
  • Call .ext() to run external methods
  • Call .hold and .free to pause/resume chains
  • Use this.sub inside method to run a subchain
  • Call .swap() and .back to jump between chains
  • Control chain swapping data flow with .sync()

Reference

static from(classRef)

Creates a chainable API from a class that extends Unchain.

  • classRef: Class to create chainable API from

constructor

  • datOrVal: Initial data object or values array
  • valOrDat: Complementary values array or data object

Properties

  • this.val: Array of values returned by chained methods
  • this.cur: Values returned by previous chained mathod call
  • this.dat: Data object available throughout chain execution
  • this.sub: Triggers subchain execution

.mode(outputMode)

Sets chain output, returned by final promise:

  • "vals": All chained method return values array (default)
  • "last": Last chained method return value
  • "data": Chain data object this.dat
  • "this": Chain instance

.sync(syncMode)

Sets chain swapping synchronization mode:

  • "full": Sync values and data (default)
  • "vals": Sync only output values array this.val
  • "data": Sync only chain data this.dat
  • "self": No sync

.ext(callback)

Executes external method within the chain. Return value is pushed to chain output.

  • callback: Method to execute, receives chain instance argument

.hold

Pauses chain execution. Returns a Promise with chain reference to resume later.

.free

Resumes a paused chain.

.swap(classRef, ...args)

Creates a new chain of the specified class and switches execution context

  • classRef: chainable class to create and swap to
  • ...args: arguments passed to new chain constructor
  • If called without arguments, swaps to the most recent chain again
  • If called again with argument, disposes previous chain link and rewires to new instance

.back

Swaps back to previous chain.

Examples

Lorem ipsum

// is this code ?
SpeaksLatin.lorem.ipsum.dolor.sit
.amet.consectetur.adipiscing.elit
.sed.do.eiusmod.tempor.incididunt
.ut.labore.et.dolore.magna.aliqua
.then(console.warn);

run
clear
IsThisCode

const loremIpsum = "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua";

const SpeaksLatin = Unchain.from(
loremIpsum.split(" ").reduce(
	(typing, word) => {
		Object.defineProperty(
			typing.prototype,
			word,
			{
				value: async () => {
					await wait();
					return word;
				}
			}
		);
		return typing;
	}, 
	class LoremIpsum extends Unchain {}
));

Fibonacci

Fibonacci
.fibonacci
.fibonacci
.fibonacci
.fibonacci
.fibonacci
.fibonacci
.then(console.warn);

run
clear
Fibonacci

const Fibonacci = Unchain.from(
class Fibonacci extends Unchain {

	constructor() {
		super([0, 1]);
		console.log(...this.val);
	}

	async fibonacci() {
		await wait();
		return this.cur 
		+ this.val.at(-2);
	}

});

Arrays

const Arrays = Unchain.from(
	class Arrays extends Unchain {

		set(...val) {
			return val;
		}

		add(val) {
			return this.cur
			.map(v => v + val);
		}

		rev() {
			return this.cur
			.toReversed();
		}

	}
);
Arrays
.mode("last")
.set(1, 2, 3)
.add(1)
.rev
.then(console.log)
.finally(chn => console.warn(chn));

run
clear
ArrayOps

Alphabet

Subchain swapping nasty test

Alphabet.char // "A"
.next.next.next.next.next // BCDEF
.next.next.next.next.next // GHIJK
.next.next.next.next.next // LMNOP
.next.next.next.next.next // QRSTU
.next.next.next.next.next // VWXYZ
.then(console.warn);

run
clear
Alphabet

class AlphaChain extends Unchain {

	constructor(code = 65) {
		super({code: code});
	}

	next() {
		return this.sub.swap(Alphabet, this.dat.code + 1).char.back;
	}

	async letter() {
		await wait();
		return String.fromCharCode(this.dat.code);
	}

}

const Alphabet = Unchain.from(AlphaChain);

Ping-pong

const PingPong = Unchain.from(
	class PingPong extends Unchain {

		constructor(name) {
			super({[name]: 0});
			this.name = name;
		}

		async hit() {
			await wait();
			return this.name + " " + (++this.dat[this.name]);
		}

		miss() {
			return this.name + " miss !";
		}

	}
);
PingPong("ping") // create Ping
.mode("data") // data mode
.hit // ping 1
.swap(PingPong, "pong") // new pong
.hit // pong 1
.back // back to ping
.hit // ping 2
.swap // swap to pong
.hit // pong 2
.back.hit // ping 3
.swap.miss // pong miss
.then(console.log);

run
clear
PingPongGame

Chain generator

Type some text to generate a chainable class template.

  • use parentheses to define method arguments
  • use parentheses and braces to store data
<main id="chaingen">
	<pre><code id="output" class="language-js"></code></pre>
	<input type="text" id="input" placeholder="Greet(a, b) hello world hi(who)">
</main>
#chaingen {
	display: flex;
	flex-direction: column;
	overflow: hidden;
	gap: .5rem;
}

#chaingen input {
	background: var(--code-bg);
	border: none;
	box-sizing: border-box;
	width: 100%;
	outline: none;
	font-family: var(--code-font);
	color: inherit;
	font-size: inherit;
	height: 2rem;
	padding-left: .5rem;
}

#chaingen pre {
	margin: 0;
	padding: var(--pad);
	width: 100%;
	min-height: 1rem;
	height: auto;
	box-sizing: border-box;
	font-family: var(--code-font);
}
1.02k
const input = document.getElementById("input"); const output = document.getElementById("output"); input.addEventListener("input", () => { const chain = input.value.trim(); if(!chain) { output.innerHTML = ""; return; } const [cls, ...methods] = Array.from( chain.matchAll(/(\w+)(?:\(\{?([^\}\)]+)(\}?)\)?)?/g) ).map(part => ({ nnm: part[1], prm: (part[2] || "") .split(",") .map(p => p.trim()) .filter(Boolean), dat: part[3] != "" })); output.innerHTML = ` class ${cls.nnm}Chain extends Unchain { constructor(${cls.prm.join(", ")}) { super(${cls.dat && cls.prm.length ? `{ ${cls.prm.join(", ")} }` : ""}); } ${methods.map( m => ` ${m.nnm}(${m.prm.join(", ")}) {${m.dat ? m.prm.map(p => ` this.dat.${p} = ${p};`).join("") : ""} return \`${m.nnm}${m.prm.length ? " " + m.prm.map(p => `\${${p}}`).join(" ") : ""}\`; } `).join("")} } const ${cls.nnm} = Unchain.from(${cls.nnm}Chain);`; High.light(output); }); input.value = "Greet(a, b) hello world hi(who)"; input.dispatchEvent(new Event("input"));

Unchain + Puppeteer

PuppetX.speed(1).mode("data").work(workdir).full.curtain.goto("https://google.com/").xpath.any.div.attr("aria-live").is("polite").after.deep.button.div.role.is("none").xpost(2).when.scroll.time.click.time.xpath.any.textarea.name.is("q").find.hover.click.time.write("github " + profile).time.enter.idle.time.xpath.any.a.tree.h3.text.has(profile).find.hover.click.idle.time.xpath.any.a.attr("data-selected-links").has("repositories").find.click.idle.time.xpath.any.li.class.has("source").deep.h3.a.href.has(project).find.hover.click.idle.time.xpath.any.a.text.is("Demo").find.hover.click.idle.time.clickxy(200, 200).clickxy(350, 300).clickxy(450, 400).time(10000).exit.then(console.log).catch(console.error).finally(() => console.log("the end"));

PuppetChain

Test n°1 : Puppeteer + XPath

Messy Puppeteer core methods wrapper with XPath query builder.
Here is the commented version of previous chain code.

Test n°2 : Chain swapping

Distinct Puppeteer & XPath classes (11kB + 8kB).

class PuppetShow extends Unchain {
	// ...
}

class XPath extends Unchain {
	// ...
}

Test n°3 : Subchain swapping

Puppet3

Google & Github chains

const Googling = require("../chains/google.js");
const Githubing = require("../chains/github.js");

const website = "github";
const profile = "nicopowa";
const project = "ripples3";

return Googling // puppet show
.dotCom(datadir, workdir) // init
.noThanks // want some cookies ?
.search(website, profile) // search
.result(website, profile) // find
.swap(Githubing) // github profile
.profileRepos // repo list
.loadRepo(project) // load repo

// readme demo link
.time // delay
.xph // xpath mode
.any // anywhere
.a // anchor link
.text // with text
.is("Demo") // equals
.compute // query
.back // back
.find // demo link
.hover // hover
.click // click

// load & click demo
.idle // wait load
.time // delay
.clicks([200, 200], [350, 300], [450, 400])

// the end
.time(5000) // exit soon
.exit // close browser
.then(console.log) // output
.catch(console.error) // fail
.finally(() => // next chain
	console.log("the end"));

Next : more subchains

XPath find link by href : `this.sub .a // anchor link .href // href attr .has(findhref); // contains


Realistic mouse actions, ...


## Ideas

- FFmpeg command line builder and runner
- Database query builder
- Client / server communications
- ...

## Unchain code


[Github](https://github.com/nicopowa/unchain)