Coding soon

Coding soon

Unchain

Funny experiments with Javascript Proxy

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

Promise-based
Clear syntax
Error handling
Nested chains
Chain swapping
Cheat sheet
Reference
Examples
Generator
Puppet
Ideas
Code

Hello Unchain#

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

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

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

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

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

}

const Testing = Unchain.from(TestChain);
new Testing().chained()
.asynchronous().methods()
.and().then(console.warn);
run
clear

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

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

Or await

console.log(
	await new Chaining()
	.synchronous().and()
	.asynchronous().methods()
);
run
clear

Clear syntax#

No more return this;

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

Create chain without new keyword

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

Call methods without parentheses if not passing arguments

Chain.hello.world
.then(console.log);
run
clear
Chaining.synchronous.and
.asynchronous.methods
.then(console.log);
run
clear

Use parentheses when passing arguments

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

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
Unstable.test.ohNo
.then(console.log)
.catch(console.error)
.finally(failChain => 
	console.log(failChain.val)
);
run
clear
try {
	console.log(await Unstable.test.ohNo);
}
catch(err) {
	console.error(err);
}
finally {
	console.log("what now ?");
}
run
clear

Method does not exist

Unstable.test.oops
.then(console.log)
.catch(console.error)
.finally(failChain => 
	console.log(failChain.val)
);
run
clear
try {
	console.log(await Unstable.test.oops);
}
catch(err) {
	console.error(err);
}
finally {
	console.log("what now ?");
}
run
clear

Chain data#

Unchain provides two storage mechanisms:

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

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.

Previous method return value can be accessed via this.cur.

Output Modes#

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

class Box extends Unchain {
	constructor() {
		super({items: []});
	}
	
	store(item) {
		this.dat.items.push(item);
		return `stored: ${item}`;
	}
}

const LunchBox = Unchain.from(Box);

"vals": default, output methods return values array

LunchBox
.store("apple")
.store("banana")
.then(console.warn);
run
clear

"data": output chain data object

LunchBox
.mode("data")
.store("apple")
.store("banana")
.then(console.warn);
run
clear

"last": output last chained call return value

LunchBox
.mode("last")
.store("apple")
.store("banana")
.then(console.warn);
run
clear

"this": output chain instance

LunchBox
.mode("this")
.store("apple")
.store("banana")
.then(lunch => {
	console.warn(lunch.val);
	console.warn(lunch.dat);
});
run
clear

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

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

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.warn);
	}
);
run
clear

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.warn);
run
clear

Deep subchains#

class AllStepsChain extends SubStepsChain {

	// oneTwoThreeFour = oneTwo + threeFour
	oneTwoThreeFour() {
		return this.sub
		.oneTwo.threeFour;
	}
	
}

const AllSteps = Unchain.from(AllStepsChain);
AllSteps.oneTwoThreeFour // 1 & 2 + 3 & 4
.then(console.warn);
run
clear

Inside chains#

Run subchains without return statement to perform other tasks. Values will not be pushed to chain output.

class TasksChain extends Unchain {

	async task1() {
		await wait();
		return "task1";
	}

	async task2() {
		await wait();
		return "task2";
	}

	async more1() {
		await wait();
		console.log("more1");
	}

	async more2() {
		await wait();
		console.log("more2");
	}

	async tasks12() {
		await this.sub.more1.more2;
		return this.sub.task1.task2;
	}

	async task3() {
		await wait();
		return "task3";
	}
	
}

const Tasks = Unchain.from(TasksChain);
Tasks.tasks12.task3
.then(console.warn);
run
clear

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.warn);
run
clear

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.warn);
run
clear

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.warn);
run
clear

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.warn);
run
clear

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.warn);
// A B C A B C A C B A
run
clear

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.warn);
run
clear

Data sync#

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

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

// example soon

@boxed name:ValsSync nobody nofull height:7 Unchain,Count,ValsSync

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

Main RGB class gathering R, G and B components values one by one

RGB Chains
+
const RGB = Unchain.from(
	class RGB extends Unchain {}
);

class Component extends Unchain {

	constructor(name) {
		super();
		this.name = name;
	}

	value(v) {
		this.dat[this.name] = v;
		console.log(this.dat);
		return this.name + " " + v;
	}

}

const [Red, Green, Blue] = [
	class Red extends Component {
		constructor() {
			super("red");
		}
	}, 
	class Green extends Component {
		constructor() {
			super("green");
		}
	},
	class Blue extends Component {
		constructor() {
			super("blue");
		}
	}
].map(Unchain.from);
RGB // new RGB
.mode("data") // data mode
.sync("full") // full sync
.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.warn) // orange
.finally(rgb => // output
	console.warn(rgb.val));
run
clear

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

// example soon

@boxed name:DataSync nobody nofull height:7 Unchain,DataSync

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

// example soon

@boxed name:SelfSync nobody nofull height:7 Unchain,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(swapMode)#

Sets chain swapping synchronization mode

  • "vals": Sync only output values array this.val (default)
  • "full": Sync both values and data
  • "data": Sync only 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
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
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);
	}

});

Binary#

Binary
.zero.one.zero.one.one.zero.one.zero
.then(console.warn)
run
clear
const Binary = Unchain.from(
class BinChain extends Unchain {

	zero() {
		return 0;
	}

	one() {
		return 1;
	}

});

Arrays#

Transform array, keep history, output last state

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") // last mode
.set(1, 2, 3) // set values
.add(1) // map +1
.rev // reverse
.then(console.warn) // output
.finally(chn => // history
	console.warn(chn.val));
run
clear

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
.sync("data") // data sync
.hit // ping 1
.swap(PingPong, "pong") // => pong
.hit // pong 1
.back // ping <-
.hit // ping 2
.swap // -> pong
.hit // pong 2
.back.hit // ping 3 <-
.swap.miss // -> pong miss
.then(console.warn);
run
clear

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="ljs"></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);
}
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#

It took a while to write this tiny piece of code, now let's make everything look like jQuery 🤓
Here is the first messy experiment.

PuppetX.speed(1).sync("full").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"));
PuppetChainPuppetChain

Test n°1 : Puppeteer + XPath#

Messy Puppeteer core methods wrapper with XPath query builder.

Same code with comments:

PuppetShow 1
+
PuppetShow // puppet show
.speed(1) // 0 = jedi -> 10 = potato
.mode("data") // data mode
.sync("full") // full sync
.work(workdir) // working directory
//.less // headless
.full // not headless

// classic puppeteer
// .init // init puppeteer
// .open // launch browser
// .page // create new page
// .ua("chrome") // user agent

// real browser + adblocker
.curtain // call
// .view(1280, 800) // viewport size

// google
.goto("https://google.com/") // .com
// .shot("home") // screenshot

// i don't care about cookies
.xpath // xpath mode
.any // anywhere
.div // div
.attr("aria-live") // with attr
.is("polite") // really ?
.after // next element
.deep // child tree
.button // button
.div // child div
.role // with role
.is("none") // equals
.xpost(2) // reject = index 2
.when // wait for matches
.scroll // scroll to 
.time // delay
.click // click reject

// google search
.time // delay
.xpath // xpath mode
.any // anywhere
.textarea // textarea
.name // with name
.is("q") // equals
.find // run xpath
.hover // hover
.click // click
.time // delay
.write("github " + profile) // type
.time // delay
.enter // press enter

// google results
.idle // wait load
.time // delay
// .shot("search") // shoot
.xpath // xpath mode
.any // anywhere
.a // anchor link
.tree // child tree
.h3 // h3 heading
.text // with text
.has(profile) // contains
.find // run xpath
.hover // hover it
.click // click link

// hello GitHub
.idle // wait load
.time // delay
// .shot("github") // shoot
.xpath // xpath mode
.any // anywhere
.a // anchor link
.attr("data-selected-links") // attr
.has("repositories") // equals
.find // run xpath
.click // click

// repos
.idle // wait load
.time // delay
.xpath // xpath mode
.any // anywhere
.li // li element
.class // with class
.has("source") // contains
.deep // and subtree
.h3 // h3 element
.a // anchor element
.href // href attr
.has(project) // contains
.find // run xpath
.hover // hover
.click // click

// project
.idle // wait load
.time // delay
.xpath // xpath mode
.any // anywhere
.a // anchor link
.text // with text
.is("Demo") // equals
.find // run xpath
.hover // hover
.click // click

// demo
.idle // wait load
.time // delay
.clickxy(200, 200) // click here
.clickxy(350, 300) // and there
.clickxy(450, 400) // one more

// .shot("done") // shoot
.time(10000) // wait debug
.exit // close browser
.then(result => {

	// console.log(result);

})
.catch(err => {

	console.log("puppet fail");
	console.error(err);

})
.finally(() => {

	console.log("the end");

});

Test n°2 : Chain swapping#

Distinct Puppeteer & XPath classes

class PuppetShow extends Unchain {
	// ...
}

class XPath extends Unchain {
	// ...
}

Test n°3 : Subchain swapping#

Same but differentSame but different

Handling Google and Github from separate classes.

Replaced .find by horrible .compute.back.find to manage chain swapping.

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"));
Google chain
+
class Googling extends PuppetShow {

	constructor(options = {}) {
		super(options);
		this.url = "https://google.com/";
	}

	dotCom(datadir, workdir) {

		// open browser & google 

		return this.sub
		.speed(1) // 0 = jedi -> 10 = potato
		.sync("full") // chain full sync
		.data(datadir) // set browser dir (data & ublock)
		.work(workdir) // set working dir
		.full // not headless
		.curtain // real browser + adblocker
		.goto(this.url);
	
	}
	
	noThanks() {

		// i don't care about cookies

		return this.sub // chain
		.xph // xpath mode
		.any // anywhere
		.div // div
		.attr("aria-live") // with attr
		.is("polite") // really ?
		.after("span") // next span
		.deep // child tree
		.button // button
		.div // div
		.role // with role
		.is("none") // equals
		.compute // query
		.back // back
		.when // wait for matches
		.post(2) // select idx 2 reject
		.scroll // scroll to element
		.time // delay
		.click; // no thanks
	
	}

	search(...terms) {

		// google search

		return this.sub // chain
		.xph // xpath mode
		.any // anywhere
		.textarea // textarea
		.name // with name
		.is("q") // equals
		.compute // query
		.back // back
		.find // search box
		.hover // hover
		.click // click
		.time // delay
		.type(terms.join(" ")) // type
		.time // delay
		.enter // press enter
		.idle; // and wait
	
	}

	result(website, title) {
		
		// parse results

		return this.sub // chain
		.time // delay
		.xph // xpath mode
		.any // anywhere
		.a // anchor link
		.href // with href attr
		.has(website) // contains
		.deep // and child tree
		.h3 // heading
		.text // with text
		.has(title) // contains
		.compute // query
		.back // back
		.find // result link
		.scroll // if needed
		.hover // hover it
		.click // click it
		.idle; // and wait
	
	}
 
}

const Google = Unchain.from(Googling);
Github chain
+
class Githubing extends PuppetShow {

	constructor(options = {}) {

		super(options);
		this.url = "https://github.com/";
	
	}

	dotCom(datadir, workdir) {
		// TODO standalone Github
	}

	search(what) {
		// TODO Github search
	}

	profileRepos() {
		
		// profile > repositories

		return this.sub // chain
		.time // delay
		.xph // xpath mode
		.any // anywhere
		.a // anchor link
		.attr("data-selected-links") // attr
		//.data("selected-links") // same same
		.has("repositories") // contains
		.compute // query
		.back // back
		.when // repos list link
		.hover // hover
		.click // click
		.idle; // and wait
	
	}

	loadRepo(project) {

		// repo list > repo

		return this.sub
		.time // delay
		.xph // xpath mode
		.any // anywhere
		.li // list item
		.class // class attr
		.has("source") // contains
		.deep // child tree
		.h3 // heading
		.a // anchor link
		.href // href attr
		.has(project) // contains
		.compute // query
		.back // back
		.find // repo link
		.hover // hover
		.click // click
		.idle; // and wait
	
	}
 
}

const Github = Unchain.from(Githubing);

Test n°4 : Subchains °⋆。𖦹#

Simple chain to click ripples on this website

PuppetX // puppet show
.speed(1) // 0 = jedi -> 10 = potato
.mode("data") // data mode
.sync("full") // full sync
.full // not headless
.init // load puppeteer
.open // open browser
.page // new page
.goto("https://nicopr.fr")
.time // delay
.xph // xpath mode
.any // anywhere
.a // anchor link
.href // href attr
.has("ripples3") // contains
.compute // build query
.back // back to Puppet
.find // exec query
.click // click link
.idle // wait load
.xph // xpath
.any // anywhere
.a // anchor link
.href // href attr
.has("frame") // contains
.compute // build
.back // back
.find // find
.click // click
.idle // wait
.time // delay
.clicks([200, 200], [350, 300], [450, 400])
// .time.exit.then.catch

Write subchains

// PuppetShow browser init
run() {

	return this.sub
	.speed(1) // 0 = jedi -> 10 = potato
	.mode("data") // data mode
	.sync("full") // full sync
	.full // not headless
	.init // load puppeteer
	.open // open browser
	.page; // new page

}
// XPath link search
link(findhref) {

	return this.sub
	.any // anywhere
	.a // anchor link
	.href // href attr
	.has(findhref); // contains

}
// PuppetShow link search shortcut
link(href) {

	return this.sub // subchain
	.xph // xpath mode
	.link(href) // link subchain
	.compute // build query
	.back // come back here
	.find; // run query

}

Update chain

PuppetX // puppet show
.run // open browser
.goto("https://nicopr.fr")
.time // delay
.link("ripples3") // find
.click // click link
.idle // wait load
.link("frame") // link
.click // click
.idle // wait
.time // delay
.clicks([200, 200], [350, 300], [450, 400])
// .time.exit.then.catch

More subchains :)

PuppetX.run
.goto("https://nicopr.fr")
.clickLink("ripples3")
.clickLink("frame")
.clicks([200, 200], [350, 300], [450, 400])
// .time.exit.then.catch
PuppetX.run
.goto("https://nicopr.fr")
.places("ripples3", "frame")
.clicks(3) // 3x random click
// .time.exit.then.catch

Next#

  • CSS chain
  • Hybrid XPath + CSS chain
  • Seamless chained shadow root navigation

Ideas#

FFmpeg#

FFmpeg
.input("source.mp4")
// common
.videoCodec("libx264")
.audioCodec("aac")
// 720p
.output("out_720p.mp4")
.size(1280, 720)
.videoBitrate(1000)
.audioBitrate(96)
// 1080p
.output("out_1080p.mp4")
.size(1920, 1080)
.videoBitrate(5000)
.audioBitrate(192)
// run cmd
.overwrite
.encode
.then(console.log);

Add .codecs & .bitrates suchains

FFmpeg
.input("source.mp4")
// common
.codecs("libx264", "aac")
.overwrite
// 720p
.output("out_720p.mp4")
.size(1280, 720)
.bitrates(1000, 96)
// 1080p
.output("out_1080p.mp4")
.size(1920, 1080)
.bitrates(5000, 192)
// run cmd
.overwrite
.encode
.then(console.log);

Twin client/server chains#

TODO

Unchain code#

unchain.js
+
/**
 * @force
 * @export
 * @class UCHN : Unchain — Nico Pr — https://nicopr.fr/unchain
 */
class UCHN {

	/**
	 * @construct
	 * @param {!(Object|Array)=} datOrVal
	 * @param {!(Array|Object)=} valOrDat
	 */
	constructor(datOrVal, valOrDat) {

		this.uid = Math.random()
		.toString(36)
		.slice(
			2,
			6
		)
		.toUpperCase();

		this.who = this.constructor.name;

		// if(UCHN_VERB) console.log(this.who + (UCHN_UIDS && this.uid || ""));

		this.val = [];
		this.dat = {};
		this.mod = "vals";
		this.snc = "vals";
		this.prv = null;
		this.nxt = null;
		this.sbc = 0;

		if(Array.isArray(datOrVal))
			this.val = datOrVal;
		else if(datOrVal)
			this.dat = datOrVal;

		if(Array.isArray(valOrDat))
			this.val = valOrDat;
		else if(valOrDat)
			this.dat = valOrDat;

		this.cur = this.val.length ? this.val.at(-1) : null;
	
	}

	/**
	 * @force
	 * @export
	 * @getter
	 * @type {Proxy}
	 */
	get sub() {

		this.sbc++;

		return UCHN.chain(this);
	
	}

	/**
	 * @force
	 * @export
	 * @method mode
	 * @param {string} mod
	 */
	mode(mod) {

		this.mod = mod;

		return this;
	
	}

	/**
	 * @force
	 * @export
	 * @method sync
	 * @param {string} snc
	 */
	sync(snc) {

		this.snc = snc;

		return this;
	
	}

	/**
	 * @force
	 * @export
	 * @method swap
	 * @param {function(new:Unchain)} TargetChain
	 * @param {...*} args
	 */
	swap(TargetChain, ...args) {

		if(!TargetChain) {

			const dst = this.nxt;

			if(!dst)
				UCHN.fail("no next");

			UCHN.sync(
				this,
				1,
				dst
			);

			return dst;
		
		}
		
		const dst = new TargetChain.baseClass(...args);

		dst.prv = this;
		this.nxt = dst;
		
		UCHN.sync(
			this,
			1,
			dst,
			true
		);

		return dst;
	
	}
	
	/**
	 * @force
	 * @export
	 * @getter
	 * @type {Proxy}
	 */
	get back() {

		const dst = this.prv;

		if(!dst)
			UCHN.fail("no prev");
		
		UCHN.sync(
			dst,
			0,
			this
		);

		return dst;
	
	}

	/**
	 * @force
	 * @export
	 * @static
	 * @method from
	 * @param {function(new:Unchain)} ClassRef
	 */
	static from(ClassRef) {

		const factory = function(...args) {

			return UCHN.chain(new ClassRef(...args));
		
		};

		factory.baseClass = ClassRef;
		
		return new Proxy(
			factory,
			{
				construct: (_, args) =>
					UCHN.chain(new ClassRef(...args)),
				get: (_, prop) =>
					typeof ClassRef.prototype[prop] === "function"
						? UCHN.chain(new ClassRef())[prop]
						: Reflect.get(
							factory,
							prop
						)
			}
		);
	
	}

	static proc(chn, res) {

		if(UCHN_VERB && res !== undefined)
			console.log(res);

		if(res !== undefined)
			chn.val.push(chn.cur = res);

		return chn;
	
	}

	static chain(init) {

		const ctx = {
			cur: init,
			ops: []
		};

		const keep = new Map();
		
		const exec = async op => {

			try {

				const res = await op(ctx.cur);

				if(res instanceof UCHN)
					ctx.cur = res;

				return ctx.cur;
			
			}
			catch(err) {

				throw err;
			
			}
		
		};
		
		const flush = async () => {

			if(!ctx.ops.length)
				return ctx.cur;

			const ops = [...ctx.ops];

			ctx.ops = [];

			for(const op of ops)
				await exec(op);

			return ctx.cur;
		
		};
		
		const callop = (prop, args = []) =>
			async chn => {

				if(!chn[prop] || typeof chn[prop] !== "function") 
					UCHN.fail("no method " + prop + " in " + chn.who);

				const res = await chn[prop].apply(
					chn,
					args
				);

				return res instanceof UCHN 
					? res 
					: UCHN.proc(
						chn,
						res
					);
		
			};
		
		const chain = new Proxy(
			{},
			{
				get(_, prop) {

					if(prop === "then") {

						return (enjoy, eject) => {

							const sink = flush()
							.then(chn => {

								if(chn.sbc) {

									chn.sbc--;

									return undefined;
							
								}
								
								if(chn.mod === "data") 
									return chn.dat;

								if(chn.mod === "last") 
									return chn.cur;

								if(chn.mod === "this") 
									return chn;

								return chn.val;
						
							});
						
							const prom = eject 
								? sink.then(
									enjoy,
									eject
								) 
								: sink.then(enjoy);

							const tcf = ["then", "catch", "finally"];
						
							tcf.forEach(ise => {

								const orig = prom[ise];

								prom[ise] = function(...args) {

									const sed = orig.apply(
										this,
										ise === "finally" 
											? [() =>
												args[0](ctx.cur)] 
											: args
									);

									tcf.forEach(m =>
										sed[m] = prom[m]);

									return sed;
							
								};
						
							});
						
							return prom;
					
						};
				
					}
				
					if(prop === "catch") {

						return caught => {

							flush()
							.catch(caught);

							return chain;
					
						};
				
					}
				
					if(prop === "finally") {

						return fn => {

							flush()
							.finally(() =>
								fn(ctx.cur));

							return chain;
					
						};
				
					}
				
					if(prop === "ext") {

						return fn => {

							ctx.ops.push(async chn => 
								UCHN.proc(
									chn,
									await fn(chn)
								));

							return chain;
					
						};
				
					}
				
					if(prop === "hold") {

						ctx.ops.push(chn => {

							Object.defineProperty(
								chn,
								"free",
								{ 
									get: () => 
										UCHN.chain(chn),
									configurable: true
								}
							);

							return chn;
						
						});
						
						const flushed = () => 
							flush(); 
						
						return {
							then: (resolve, reject) => {

								return flushed()
								.then(
									resolve,
									reject
								);
							
							},
							catch: reject => {

								return flushed()
								.catch(reject);
							
							}
						};
					
					}
				
					if(prop === "swap") {

						return new Proxy(
							function(TargetChain, ...args) {

								ctx.ops.push(chn =>
									!TargetChain 
										? chn.swap() 
										: chn.swap(
											TargetChain,
											...args
										));

								return chain;
						
							},
							{
								get: (_, nxt) => {

									ctx.ops.push(chn =>
										chn.swap());

									return chain[nxt];
							
								}
							}
						);
				
					}
				
					if(prop === "back") {

						ctx.ops.push(chn =>
							chn.back);

						return chain;
				
					}

					if(!keep.has(prop)) {

						keep.set(
							prop,
							new Proxy(
								function() {},
								{
									apply: (_, __, args) => {

										ctx.ops.push(callop(
											prop,
											args
										));

										return chain;
						
									},
									get: (_, nxt) => {

										ctx.ops.push(callop(prop));

										return chain[nxt];
						
									}
								}
							)
						);
				
					}
				
					return keep.get(prop);
			
				},
				apply: () => 
					chain
			}
		);
		
		return chain;
	
	}

	static sync(src, dir, dst, mrg = false) {

		if(UCHN_VERB) 
			console.log(
				// dir ? "SWAP" : "BACK", 
				src.who + (UCHN_UIDS ? ":" + src.uid : ""),
				dir ? (mrg ? "=" : "-") + ">" : "<-",
				dst.who + (UCHN_UIDS ? ":" + dst.uid : "")
			);

		if(!dir) 
			[src, dst] = [dst, src];
		
		if(src.snc === "vals" || src.snc === "full") {

			dst.val = mrg 
				? [...src.val, ...dst.val] 
				: [...src.val];
			
			// if(dst.val.length) // ?
			dst.cur = dst.val.at(-1);
		
		}
		
		if(src.snc === "data" || src.snc === "full") 
			dst.dat = {
				...dst.dat, ...src.dat
			};
		
		dst.mod = src.mod;
		dst.snc = src.snc;
	
	}

	static fail(err) {

		throw new Error(err);
	
	}

}

/**
 * @define {boolean} UCHN_VERB : verbose
 */
const UCHN_VERB = true;
/**
 * @define {boolean} UCHN_UIDS : outloud
 */
const UCHN_UIDS = false;

/**
 * @force
 * @export
 * @class Unchain : 
 */
const Unchain = UCHN;

if(typeof module !== "undefined" && module.exports)
	module.exports = UCHN;
unchain.min.js
+
/**
 * Nico Pr — https://nicopr.fr/unchain
 * UNCHAIN STANDALONE RELEASE v0.1.3
 * 14/05/2025 03:41:28
 */
(function(){
var k=this||self;function p(a){var d=t;a=a.split(".");var b=k;a[0]in b||"undefined"==typeof b.execScript||b.execScript("var "+a[0]);for(var f;a.length&&(f=a.shift());)a.length||void 0===d?b[f]&&b[f]!==Object.prototype[f]?b=b[f]:b=b[f]={}:b[f]=d};function u(a){var d=a,b=[];const f=new Map,C=async m=>{try{const e=await m(d);e instanceof t&&(d=e);return d}catch(e){throw e;}},q=async()=>{if(!b.length)return d;const m=[...b];b=[];for(const e of m)await C(e);return d},w=(m,e=[])=>async c=>{if(!c[m]||"function"!==typeof c[m])throw Error("no method "+m+" in "+c.A);const g=await c[m].apply(c,e);return g instanceof t?g:v(c,g)},l=new Proxy({},{get(m,e){if("then"===e)return(c,g)=>{const n=q().then(h=>{if(h.o)h.o--;else return"data"===h.j?h.i:"last"===
h.j?h.m:"this"===h.j?h:h.g}),r=g?n.then(c,g):n.then(c),x=["then","catch","finally"];x.forEach(h=>{const D=r[h];r[h]=function(...y){const z=D.apply(this,"finally"===h?[()=>y[0](d)]:y);x.forEach(A=>z[A]=r[A]);return z}});return r};if("catch"===e)return c=>{q().catch(c);return l};if("finally"===e)return c=>{q().finally(()=>c(d));return l};if("ext"===e)return c=>{b.push(async g=>v(g,await c(g)));return l};if("hold"===e)return b.push(c=>{Object.defineProperty(c,"free",{get:()=>u(c),configurable:!0});return c}),
{then:(c,g)=>q().then(c,g),catch:c=>q().catch(c)};if("swap"===e)return new Proxy(function(c,...g){b.push(n=>c?n.l(c,...g):n.l());return l},{get:(c,g)=>{b.push(n=>n.l());return l[g]}});if("back"===e)return b.push(c=>c.back),l;f.has(e)||f.set(e,new Proxy(function(){},{apply:(c,g,n)=>{b.push(w(e,n));return l},get:(c,g)=>{b.push(w(e));return l[g]}}));return f.get(e)},apply:()=>l});return l}
function B(a,d,b,f=!1){d||([a,b]=[b,a]);if("vals"===a.h||"full"===a.h)b.g=f?[...a.g,...b.g]:[...a.g],b.m=b.g.at(-1);if("data"===a.h||"full"===a.h)b.i={...b.i,...a.i};b.j=a.j;b.h=a.h}function v(a,d){void 0!==d&&a.g.push(a.m=d);return a}
class t{constructor(a,d){Math.random().toString(36);this.A=this.constructor.name;this.g=[];this.i={};this.h=this.j="vals";this.s=this.u=null;this.o=0;Array.isArray(a)?this.g=a:a&&(this.i=a);Array.isArray(d)?this.g=d:d&&(this.i=d);this.m=this.g.length?this.g.at(-1):null}get sub(){this.o++;return u(this)}mode(a){this.j=a;return this}sync(a){this.h=a;return this}l(a,...d){if(!a){a=this.s;if(!a)throw Error("no next");B(this,1,a);return a}a=new a.v(...d);a.u=this;this.s=a;B(this,1,a,!0);return a}get back(){const a=
this.u;if(!a)throw Error("no prev");B(a,0,this);return a}}p("UCHN");t.from=function(a){function d(...b){return u(new a(...b))}d.v=a;return new Proxy(d,{construct:(b,f)=>u(new a(...f)),get:(b,f)=>"function"===typeof a.prototype[f]?u(new a)[f]:Reflect.get(d,f)})};t.prototype.swap=t.prototype.l;t.prototype.sync=t.prototype.sync;t.prototype.mode=t.prototype.mode;p("Unchain");"undefined"!==typeof module&&module.exports&&(module.exports=t);}).call(this);

Github

Supergreen 🌳#

This page should not score 99 out of 100 on Ecograder 🙃