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);
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);
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);
Or await
console.log(
await new Chaining()
.synchronous().and()
.asynchronous().methods()
);
Clear syntax
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);
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)
);
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);
}
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);
}
Chain data
Unchain provides two storage mechanisms.
this.val
: chained methods return values storage arraythis.dat
: store and share data during chain executionthis.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);
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);
"data"
: output chain data object
Handler.mode("data")
.add("one").add("two")
.then(console.log);
"last"
: output last chained call return value
Handler.mode("last")
.add("one").add("two")
.then(console.log);
"this"
: output chain instance
Handler.mode("this")
.add("one").add("two")
.then(chain =>
console.log(chain.val, chain.dat));
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);
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);
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);
}
);
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);
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);
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);
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);
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);
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);
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
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);
Data sync
Call .sync("____")
to set chain swap sync mode.
"full"
: default, sync this.val
and this.dat
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
.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
"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));
"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));
"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));
Cheatsheet
- Use
Unchain.from(classRef)
to enable chain features - Create chains without
new
keyword - Constructor
dat
and/orval
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 arrayvalOrDat
: Complementary values array or data object
Properties
this.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 objectthis.dat
"this"
: Chain instance
.sync(syncMode)
Sets chain swapping synchronization mode:
"full"
: Sync values and data (default)"vals"
: Sync only output values arraythis.val
"data"
: Sync only chain datathis.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);
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);
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));
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);
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);
Chain generator
Type some text to generate a chainable class template.
- use parentheses to define method arguments
- use parentheses and braces to store data
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"));
Test n°1 : Puppeteer + XPath
Messy Puppeteer core methods wrapper with XPath query builder.
Here is the commented version of previous chain code.
2.45kPuppetShow // puppet show .speed(1) // 0 = jedi -> 10 = potato .mode("data") // output mode .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/") // google // .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 (11kB + 8kB).
class PuppetShow extends Unchain {
// ...
}
class XPath extends Unchain {
// ...
}
Test n°3 : Subchain swapping
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"));
1.77kclass Googling extends PuppetShow { constructor(options = {}) { super(options); this.url = "https://google.com/"; } dotCom(datadir, workdir) { 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 directory .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 .write(terms.join(" ")) // type .time // delay .enter // press enter .idle; // and wait } result(website, title) { // google 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);
1.05kclass Githubing extends PuppetShow { constructor(options = {}) { super(options); this.url = "https://github.com/"; } dotCom(datadir, workdir) { // standalone Github } search(what) { // 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);
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
unchain.js
+
8.2 KB/**
* @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 = "";
if(UCHN_UIDS)
this.uid = Math.random()
.toString(36)
.slice(
2,
6
)
.toUpperCase();
this.who = this.constructor.name;
this.val = [];
this.dat = {};
this.mod = "vals";
this.snc = "full";
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);
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;
}
switch(chn.mod) {
case "this": return chn;
case "data": return chn.dat;
case "last": return chn.cur;
default: 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 res = orig.apply(
this,
ise === "finally" ? [() =>
args[0](ctx.cur)] : args
);
tcf.forEach(m =>
res[m] = prom[m]);
return res;
};
});
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 => {
const res = await fn(chn);
return UCHN.proc(
chn,
res
);
});
return chain;
};
}
if(prop === "hold") {
ctx.ops.push(chn => {
Object.defineProperty(
chn,
"free",
{
get: () =>
UCHN.chain(chn),
configurable: true
}
);
return chn;
});
const flushPromise = () =>
flush();
return {
then: (resolve, reject) => {
return flushPromise()
.then(
resolve,
reject
);
},
catch: reject => {
return flushPromise()
.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 = mrg ? {
...src.dat, ...dst.dat
} : {
...src.dat
};*/
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
+
3 KB/**
* Nico Pr — https://nicopr.fr/unchain
* UNCHAIN STANDALONE RELEASE v0.0.8
* 18/04/2025 19:08:24
*/
(function(){
var h=this||self;function p(a){var d=t;a=a.split(".");var b=h;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 n=>{try{const e=await n(d);e instanceof t&&(d=e);return d}catch(e){throw e;}},q=async()=>{if(!b.length)return d;const n=[...b];b=[];for(const e of n)await C(e);return d},w=(n,e=[])=>async c=>{if(!c[n]||"function"!==typeof c[n])throw Error("no method "+n);const g=await c[n].apply(c,e);return g instanceof t?g:v(c,g)},m=new Proxy({},{get(n,e){if("then"===e)return(c,g)=>{const l=q().then(k=>{if(k.m)k.m--;else switch(k.j){case "this":return k;case "data":return k.h;
case "last":return k.o;default:return k.g}}),r=g?l.then(c,g):l.then(c),x=["then","catch","finally"];x.forEach(k=>{const D=r[k];r[k]=function(...y){const z=D.apply(this,"finally"===k?[()=>y[0](d)]:y);x.forEach(A=>z[A]=r[A]);return z}});return r};if("catch"===e)return c=>{q().catch(c);return m};if("finally"===e)return c=>{q().finally(()=>c(d));return m};if("ext"===e)return c=>{b.push(async g=>{const l=await c(g);return v(g,l)});return m};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(l=>c?l.l(c,...g):l.l());return m},{get:(c,g)=>{b.push(l=>l.l());return m[g]}});if("back"===e)return b.push(c=>c.back),m;f.has(e)||f.set(e,new Proxy(function(){},{apply:(c,g,l)=>{b.push(w(e,l));return m},get:(c,g)=>{b.push(w(e));return m[g]}}));return f.get(e)},apply:()=>m});return m}
function B(a,d,b,f=!1){d||([a,b]=[b,a]);if("vals"===a.i||"full"===a.i)b.g=f?[...a.g,...b.g]:[...a.g];if("data"===a.i||"full"===a.i)b.h=f?{...a.h,...b.h}:{...a.h};b.j=a.j;b.i=a.i}function v(a,d){void 0!==d&&a.g.push(a.o=d);return a}
class t{constructor(a,d){this.g=[];this.h={};this.j="vals";this.i="full";this.s=this.u=null;this.m=0;Array.isArray(a)?this.g=a:a&&(this.h=a);Array.isArray(d)?this.g=d:d&&(this.h=d);this.o=this.g.length?this.g.at(-1):null}get sub(){this.m++;return u(this)}mode(a){this.j=a;return this}sync(a){this.i=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](https://github.com/nicopowa/unchain)