Coding soon
Unchained
Never ending jQuery chained calls were easy to write and easy to read.
Why not chain everything then ?
Next level method chaining experiments with Javascript Proxy.
- Class to chained API
- Mix sync & async
- Clear syntax
- Handle errors
- Nested chains
- Chain swapping
Basic chaining
class DoThings {
constructor() {
console.log("let's");
this.val = [];
}
doThis() {
console.log("do this");
this.val.push("did this");
return this;
}
andThat() {
console.log("and that");
this.val.push("done that");
return this;
}
whatThen() {
return this.val;
}
}
const thingsDone = new DoThings()
.doThis()
.andThat()
.whatThen();
console.log(thingsDone);
Async chain
Write a simple async task
const wait = (time = 250) =>
new Promise(res =>
setTimeout(res, time)
);
Same class, async version
class DoThingsAsync {
constructor() {
console.log("wait while");
this.things = [];
}
async doThisLater() {
console.log("doing this");
await wait();
this.things.push("did this");
return this;
}
async andMaybeThat() {
console.log("then doing that");
await wait();
this.things.push("done that");
return this;
}
whatThen() {
return this.things;
}
}
const doingThings = await
new DoThingsAsync()
.doThisLater()
.andMaybeThat();
Oh no ! Chain is jumping to last call before running previous methods.
Callback hell
Still haunting us under a different name.
Promise
new DoThingsAsync()
.doThisLater()
.then(
doingThings =>
doingThings
.andMaybeThat()
.then(
doingThings =>
console.log(
doingThings
.whatThen()
)
)
);
Await
const doingThings = (
await (
await (
await new DoThingsAsync()
.doThisLater()
).andMaybeThat()
).whatThen()
);
console.log(doingThings);
const doingThings = new DoThingsAsync()
await doingThings.doThisLater();
await doingThings.andMaybeThat();
console.log(doingThings.whatThen())
Reduce
const reduceAsync = (
val, cbk, src) =>
val.reduce((
prm, cur,
idx, arr
) => prm.then(
acc => Promise
.resolve(cbk(
acc, cur,
idx, arr
))), Promise
.resolve(src));
reduceAsync([
"doThisLater",
"andMaybeThat",
"whatThen"],
async (acc, cur) =>
await acc[cur](),
new DoThingsAsync())
.then(console.log);
Introducing Unchained
- Easy integration
- Less boilerplate
- Mix sync & async
- Then after chain
- And catch errors
Simply extend Unchained
class, then feed it to Unchained.from
method
class Chain extends Unchained {
constructor(name = "simple") {
super();
this.val = [name]; // output
console.log(...this.val);
}
async chained() {
await wait();
return "chained";
}
async async() {
await wait();
return "async";
}
async methods(...args) {
await wait();
return "methods "
+ args.join(" ");
}
async and() {
await wait();
return "and";
}
}
const Simple = Unchained
.from(Chain);
Write the chain
new Simple("simple")
.chained()
.async()
.methods("with", "args")
.and()
.then(console.log);
Then run
Or await
console.log(
await Simple("simple")
.chained()
.async()
.methods("with", "args")
.and()
);
Easy syntax
No more return this;
async chained() {
await wait();
// return the real thing
return "chained";
}
Create instance without new
Simple("simple").chained()
.async().methods("with", "args")
.and().then(console.log);
Empty parentheses can be omitted
Simple.chained.async
.methods("with", "args")
.and.then(console.log);
Run again
Output
Chained methods return values are progressively pushed to this.val
and returned by final promise.
Call .mode("____")
to set output mode.
Available output modes :
vals
: chained methods return values (default)last
: chain last method return valuedata
: chain data storage objectthis
: chain instance
Data
Each instance has a storage object this.dat
. Use it to share data across methods and chains during execution and/or build final result if mode is set to data
.
Init
Unchained constructor accepts two optional parameters to set this.dat
and this.val
before running the chain. They can be provided in any order.
class Chain extends Unchained {
constructor(name = "simple") {
super([name]);
// super({some: "value"});
// super({some: "value"}, [name]);
// super([name], {some: "value"});
console.log(...this.val);
}
// ...
}
Errors
Use .catch
and .finally
const Unstable = Unchained.from(
class Unstable extends Chain {
constructor(name = "unstable") {
super(name);
}
async ohNo() {
await wait();
throw new Error("oh no !");
return "nope";
}
});
Unstable.chained.and.ohNo
.catch(console.error)
.finally(() =>
console.log("what happened ?")
);
Unstable.chained.and.oops
.catch(console.error)
.finally(() =>
console.log("what happened ?")
);
Extending
const Extended = Unchained.from(
class Extended extends Chain {
constructor(name) {
super(name);
}
async more() {
await wait();
return "more";
}
});
Extended("simple extended")
.chained.methods("with", "args")
.and.more.then(console.log);
Out and back
Call .ext
to run a method and return value from outside, then back to the chain.
Simple.chained.async
.ext(async simpleChain => {
console.log("be right back");
await wait(1000);
return "out and back";
})
.methods("with", "args").and
.then(console.log);
Hold on
Call .hold.then
to pause the chain, call .back
later to resume
Simple.chained.async.hold
.then(async simpleChain => {
console.log("please hold ...");
await wait(1000);
simpleChain.back
.methods("with", "args")
.and.then(console.log);
});
Nested chains
Call this.sub
inside chain methods to create a subchain.
This feature is useful to avoid code duplication or write shortcuts.
Subchain
class SubChain extends Chain {
constructor(name = "nested") {
super(name);
}
// = chained + async
chainedAsync() {
return this.sub
.chained.async;
}
// = methods + and
methodsAnd(...args) {
return this.sub
.methods(...args).and;
}
}
// keep SubChain ref
const SimpleSub = Unchained
.from(SubChain);
SimpleSub.chainedAsync
.methodsAnd("with", "args")
.then(console.log);
Deep subchain
const SimpleDeep = Unchained.from(
class DeepChain extends SubChain {
constructor(name = "deep nested") {
super(name);
}
// = chainedAsync + methodsAnd
chainedAsyncMethodsAnd(...args) {
return this.sub
.chainedAsync
.methodsAnd(...args);
}
});
SimpleDeep
.chainedAsyncMethodsAnd("with", "args")
.then(console.log);
Chain swap
Inline methods from different chain classes without nested parentheses or indentations.
Call .next(classRef)
to create and swap chain
[BUG] Chained calls arguments not preserved after swap, working on it.
const Ping = Unchained.from(
class Ping extends Unchained {
constructor() {
super({pings: 0});
console.log("Ping");
}
async ping() {
await wait();
return "ping "
+ (++this.dat.pings);
}
});
const Pong = Unchained.from(
class Pong extends Unchained {
constructor() {
super({pongs: 0});
console.log("Pong");
}
async pong() {
await wait();
return "pong "
+ (++this.dat.pongs);
}
});
Ping // create Ping
.ping // "ping 1"
.next(Pong) // new Pong swap
.pong // "pong 1"
.then(console.log);
// ["ping 1", "pong 1"]
Call .prev
with no args to go back to previous chain
Ping // create Ping
.ping // "ping 1"
.next(Pong) // new Pong swap
.pong // "pong 1"
.prev // back to Ping
.ping // "ping 2"
.then(console.log);
// ["ping 1", "pong 1", "ping 2"]
Call .prev
with no args to swap again,
back and forth between chains
Ping // create Ping
.ping // "ping 1"
.next(Pong) // new Pong
.pong // "pong 1"
.prev // back to Ping
.ping // "ping 2"
.next // forth to Pong
.pong // "pong 2"
.prev.ping.next.pong // 3
.prev.ping.next.pong // 4
.then(console.log);
Create and manage multiple chains
const MainChain = Unchained.from(
class Main extends Unchained {
constructor() {
super({value: 0});
console.log("MainChain");
}
async init(v) {
this.dat.value = v;
console.log("init", this.dat.value);
return v;
}
});
const WorkChain = Unchained.from(
class WorkChain extends Unchained {
constructor() {
super();
}
async work(data) {
await wait();
return this.dat.value *= 2;
}
});
MainChain // create main
.init(1) // init dat.value = 1
.next(WorkChain) // new worker
.work // double dat.value
.prev // sync with Main
.next // same worker
.work // double dat.value
.prev // sync with Main
.next(WorkChain) // new worker
.work // double dat.value
.prev // sync
.then(console.log); // result
Chain sync
Call .sync("____")
to choose how data should be synchronized when swapping chains.
Available sync modes :
vals
: mergethis.val
only (default)full
: mergethis.val
andthis.dat
data
: mergethis.dat
onlyself
: no sync
TODO : set different sync modes for .next
and .prev
(upstream and downstream)
Cheat sheet
New chain
new
can be omitted- init chain with output and data if needed
Syntax
- No
return this;
- No args -> No parentheses
Keywords
val
{Array} chain outputsdat
{Object} chain datamode
{Function} output modesync
{Function} sync modeext
{Function} run ext methodhold
{Function} freezeback
{Function} resumesub
{Function} subchainnext
{Function} swap chainprev
{Function} swap back
Output and errors
.then.catch.finally
Examples
Hello Unchained
const SayHi = Unchained.from(
class SayHi extends Unchained {
constructor() {
super();
}
hello() {
return "hello";
}
world() {
return "world";
}
});
const hello = await
SayHi.hello.world;
console.log(hello);
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.log);
const loremIpsum = "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua";
const SpeaksLatin = Unchained.from(
loremIpsum.split(" ").reduce(
(prototyping, word) => {
Object.defineProperty(
prototyping.prototype,
word,
{
value: async () => {
await wait();
return word;
}
}
);
return prototyping;
},
class LoremIpsum extends Unchained {}
));
Fibonacci
Fibonacci
.fibonacci
.fibonacci
.fibonacci
.fibonacci
.fibonacci
.fibonacci
.then(console.log);
const Fibonacci = Unchained.from(
class Fibonacci extends Unchained {
constructor() {
super([0, 1], {lvl: 0});
console.log(...this.val);
}
async fibonacci() {
await wait();
return this.val[this.dat.lvl]
+ this.val[++this.dat.lvl];
}
});
Calculator
const Calculator = Unchained.from(
class Calculator extends Unchained {
constructor() {
super({value: 0});
}
set(num) {
this.dat.value = num;
return "" + num;
}
add(num) {
this.dat.value += num;
return "+ " + num;
}
substract(num) {
this.dat.value -= num;
return "- " + num;
}
multiply(num) {
this.dat.value *= num;
return "x " + num;
}
divide(num) {
this.dat.value /= num;
return "÷ " + num;
}
result() {
return "= " + this.dat.value;
}
});
Calculator
.mode("data")
.set(3)
.multiply(2)
.divide(4)
.add(5)
.substract(2)
.result
.then(console.log);
Time travel
const Time = Unchained.from(
class Time extends Unchained {
constructor() {
super(["Time"]);
console.log(...this.val);
}
async warp(what) {
await wait();
return what;
}
});
// only extended, no Unchained.from
class TimeWarp extends Unchained {
constructor(datOrVal, valOrDat) {
super(datOrVal, valOrDat);
this.date = new Date();
}
}
const Past = Unchained.from(
class Past extends TimeWarp {
constructor() {
super(["Past"]);
}
async lastMonth() {
await wait();
this.date
.setMonth(this.date.getMonth() - 1);
return "last month : "
+ this.date.toLocaleDateString();
}
});
const Present = Unchained.from(
class Present extends TimeWarp {
constructor() {
super(["Present"]);
}
async today() {
await wait();
return "today : " + this.date
.toLocaleDateString();
}
});
const Future = Unchained.from(
class Future extends TimeWarp {
constructor() {
super(["Future"]);
}
async nextYear() {
await wait();
this.date
.setFullYear(this.date.getFullYear() + 1);
return "next year : "
+ this.date.toLocaleDateString();
}
});
Time // start Time travel
.warp("time warp") // from Time
.next(Past) // swap new Past
.lastMonth // from Past
.prev // time travel
.warp("past again") // nice past
.next // swap previous Past
.lastMonth // now 2 month back
.prev // time travel
.warp("past to future") //
.next(Future) // when Future
.nextYear // one year from now
.prev // back in the timewarp
.warp("bad future") // change
.next(Future) // reset Future
.nextYear // from next Future
.prev // present is just fine
.warp("let's go back") // now
.next(Present) // new Present
.today // call Present method
.then(console.log); // result
FFmpeg
Coming soon
XPath
Coming soon
Puppeteer
Unchained wrapper
class PuppetX extends Unchained {
// ...
First test :
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(1000).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"));
Commented :
PuppetX // puppet show
.speed(1) // 0 = jedi -> 10 = potato
.mode("data") // chain mode data
.work(workdir) // set 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) // set size
// google
.goto("https://google.com/") // google
// .shot("home") // screenshot
// i don't care about cookies
.xpath // xpath mode
.any // anywhere
.div // div element
.attr("aria-live") // with attr
.is("polite") // really ?
.after // next element
.deep // child tree
.button // button
.div // div element
.role // with role
.is("none") // equals
.xpost(2) // reject = index 2
.when // wait for matches
// .high // red highlight testing
.scroll // scroll to
.time // delay
.click // click reject
// google search
// js no need idle
.time // delay
.xpath // enter xpath mode
.any // anywhere
.textarea // textarea element
.name // title attribute
.is("q") // equals
.find // run xpath
.hover // hover
.click // click
.time // delay
.write("github " + profile) // type
.time // delay
.enter // press enter
// results
.idle // wait load
.time // delay
// .shot("search") // shoot
.xpath // xpath mode
.any // anywhere
.a // anchor element
.tree // subtree
.h3 // h3 element
.text // with text
.has(profile) // containing
.find // run xpath
.hover // hover element
.click // click link
// hello GitHub
.idle // wait page load
.time // delay
// .shot("github") // shoot
.xpath // xpath mode
.any // anywhere
.a // anchor element
.attr("data-selected-links") // attr
.has("repositories") // equals
.find // run xpath
.click // click
// repos
.idle // wait page load
.time // delay
.xpath // xpath mode
.any // anywhere
.li // li element
.class // with class
.has("source") // contains
.deep // and subtree
.h3 // h3 element
.a // andhor element
.href // href attr
.has(project) // contains
.find // run xpath
.hover // hover
.click // click
// project
.idle // wait load
.time(1000) // delay
.xpath // xpath mode
.any // anywhere
.a // anchor element
.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
// the end is near
// .shot("done") // shoot
.time(10000) // closing soon
.exit // close browser
.then(result => {
// console.log(result);
})
.catch(err => {
console.log("puppet fail");
console.error(err);
})
.finally(() => {
console.log("the end");
});