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
// 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);
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);
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);
Or await
console.log(
await new Chaining()
.synchronous().and()
.asynchronous().methods()
);
No more return this;
noMoreThis() {
return {any: "value"};
}
Create chain without new
keyword
Chain().hello().world()
.then(console.log);
Call methods without parentheses if not passing arguments
Chain.hello.world
.then(console.log);
Chaining.synchronous.and
.asynchronous.methods
.then(console.log);
Use parentheses when passing arguments
Chain.greet("everyone")
.then(console.log);
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)
);
Unstable.test.ohNo
.then(console.log)
.catch(console.error)
.finally(failChain =>
console.log(failChain.val)
);
try {
console.log(await Unstable.test.ohNo);
}
catch(err) {
console.error(err);
}
finally {
console.log("what now ?");
}
Method does not exist
Unstable.test.oops
.then(console.log)
.catch(console.error)
.finally(failChain =>
console.log(failChain.val)
);
try {
console.log(await Unstable.test.oops);
}
catch(err) {
console.error(err);
}
finally {
console.log("what now ?");
}
Unchain provides two storage mechanisms:
this.val
: chained methods return values storage arraythis.dat
: store and share data during chain executionConstructor 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
.
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);
"data"
: output chain data object
LunchBox
.mode("data")
.store("apple")
.store("banana")
.then(console.warn);
"last"
: output last chained call return value
LunchBox
.mode("last")
.store("apple")
.store("banana")
.then(console.warn);
"this"
: output chain instance
LunchBox
.mode("this")
.store("apple")
.store("banana")
.then(lunch => {
console.warn(lunch.val);
console.warn(lunch.dat);
});
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);
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);
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);
}
);
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);
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 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);
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);
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);
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);
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);
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
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);
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
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));
"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
Unchain.from(classRef)
to enable chain featuresnew
keyworddat
and/or val
init values (optional)return this;
this.val
this.cur
.then()
, .catch()
, .finally()
.mode()
to choose chain outputthis.dat
to share data between methods or chains.ext()
to run external methods.hold
and .free
to pause/resume chainsthis.sub
inside method to run a subchain.swap()
and .back
to jump between chains.sync()
static from(classRef)
Creates a chainable API from a class that extends Unchain.
classRef
: Class to create chainable API fromconstructor
#datOrVal
: Initial data object or values arrayvalOrDat
: Complementary values array or data objectthis.val
: Array of values returned by chained methodsthis.cur
: Values returned by previous chained mathod callthis.dat
: Data object available throughout chain executionthis.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.back
#Swaps back to previous chain
// 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);
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
.then(console.warn);
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
.zero.one.zero.one.one.zero.one.zero
.then(console.warn)
const Binary = Unchain.from(
class BinChain extends Unchain {
zero() {
return 0;
}
one() {
return 1;
}
});
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));
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);
Type some text to generate a chainable class template.
<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"));
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"));
Messy Puppeteer core methods wrapper with XPath query builder.
Same code with comments:
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");
});
Distinct Puppeteer & XPath classes
class PuppetShow extends Unchain {
// ...
}
class XPath extends Unchain {
// ...
}
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"));
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);
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);
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
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);
TODO
/**
* @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;
/**
* 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);
This page should not score 99 out of 100 on Ecograder 🙃