vario
v0.5.2
Published
Transactional, Reactive, and Asynchronous State Management for JavaScript
Downloads
47
Readme
Vario • Transactional, Reactive, and Asynchronous State Management for JavaScript
Inspired by: MobX, Nezaboodka, React, Excel.
Introduction
Vario is a transactional, reactive, and asynchronous state management library for JavaScript that is designed to be extermely lightweight, easy, and fast.
Transactivity means that multiple objects can be changed at once with full respect to the all-or-nothing principle (atomicity, consistency, and isolation). Vario maintains separate data snapshot for each transaction. The snapshot is logical and doesn't create full copy of all the data. Intermediate state is visible only inside transaction itself, but is not visible outside of transaction until it is committed. Compensating actions are not needed in case of transaction failure, because all the changes made by transaction in its logical snapshot are simply discarded.
Reactivity means that recomputation of computable objects (observers) is triggered automatically upon changes in their dependencies (observables). All the dependencies between observers and their observables are detected and maintained automatically. It is achieved by injecting property getters/setters into all objects and tracking get/set calls during execution of observer computation. Affected observers are recomputed in a proper order at the end of a transaction, when all the changes are committed.
Asynchrony means that asynchronous operations are supported as first class citizens during transaction processing. Transaction may consist of a set of asynchronous operations and being committed upon completion of all them. Moreover, any asynchronous operation may spawn other asynchronous operations, which prolong transaction execution until whole the chain of asynchronous operations is fully completed.
Differentiators
- Consistency and clarity are the first priorities
- Transactional -- full-fledged atomicity, consistency, and isolation
- Reactive -- automatic dependency tracking and fine-grained recomputation
- Asynchronous -- transaction may consist of parallel and chained asynchronous operations
- Historical -- built-in undo/redo functionality provided out of the box
- Minimalistic -- it's a tool and approach, not a framework
- Trivial -- implementation consists of less than 1000 lines of code
Demo
import { Vario, Transaction, tran, cache } from "vario";
import { Person } from "./person";
@tran
export class DemoApp {
@tran title: string = "Demo";
@tran users: Person[] = [];
@tran
loadUsers(): void {
this.users.push(new Person({
name: "John", age: 38,
emails: ["[email protected]"],
children: [
new Person({ name: "Billy" }), // William
new Person({ name: "Barry" }), // Barry
new Person({ name: "Steve" }), // Steven
],
}));
this.users.push(new Person({
name: "Kevin", age: 27,
emails: ["[email protected]"],
children: [
new Person({ name: "Britney" }),
],
}));
}
}
@tran
export class DemoAppView {
readonly model: DemoApp;
@tran userFilter: string = "Jo";
constructor(model: DemoApp) {
this.model = model;
}
@cache
filteredUsers(): Person[] {
const m = this.model;
let result: Person[] = m.users;
if (this.userFilter.length > 0) {
result = [];
for (let x of m.users)
if (x.name && x.name.indexOf(this.userFilter) === 0)
result.push(x);
}
return result;
}
@cache
render(): string[] {
// Print only those users whos name starts with filter string
let r: string[] = [];
r.push("---");
r.push(`Filter: ${this.userFilter}`);
const a = this.filteredUsers();
for (let x of a) {
let childNames = x.children.map(child => child.name);
r.push(`${x.name}'s children: ${childNames.join(", ")}`);
}
r.push("---");
return r;
}
@cache
autoprint(): void {
this.render().forEach(x => console.log(x));
}
}
export function sample(): void {
// Simple actions (transactions)
let app = new DemoApp();
let view = new DemoAppView(app);
try {
app.loadUsers();
Vario.autorenew(0, view.autoprint);
// Multi-part transaction
let t1 = new Transaction("t1");
t1.run(() => {
let daddy = app.users[0];
daddy.age += 2; // causes no execution of DemoApp.render
daddy.name = "John Smith"; // causes execution of DemoApp.render upon transaction end
daddy.children[0].name = "Barry Smith"; // Barry
daddy.children[1].name = "William Smith"; // Billy
daddy.children[2].name = "Steven Smith"; // Steve
});
t1.run(() => {
// daddy.age is 38 outside of t2 transaction, but is 40 inside t2
let daddy = app.users[0];
daddy.age += 5; // 40 + 5 = 45
view.userFilter = "";
if (daddy.emails)
daddy.emails[0] = "[email protected]";
let x = daddy.children[1];
x.parent = null;
x.parent = daddy;
});
t1.commit(); // changes are applied, caches are invalidated/renewed
// Protection from modification outside of a transaction
try {
let daddy = app.users[0];
if (daddy.emails)
daddy.emails.push("[email protected]");
else
daddy.children[1].name = "Billy Smithy";
}
catch (e) {
console.log(`Expected: ${e}`);
}
// Turn off auto renew
Vario.autorenew(-1, view.autoprint);
}
finally { // cleanup
Vario.dispose(view);
Vario.dispose(app);
}
}
/* Console output:
#vario t11 ╔═══ v10 DemoApp.ctor
#vario t11 ║ M DemoApp#11t11: title, users
#vario t11 ╚═══ v11 DemoApp.ctor - COMMIT(1)
#vario t11 gc t11 (DemoApp.ctor)
#vario t11 gc DemoApp#11t10 is ready for GC (overwritten by DemoApp#11t11}
#vario t12 ╔═══ v11 DemoAppView.ctor
#vario t12 ║ M DemoAppView#12t12: userFilter
#vario t12 ╚═══ v12 DemoAppView.ctor - COMMIT(1)
#vario t12 gc t12 (DemoAppView.ctor)
#vario t12 gc DemoAppView#12t10 is ready for GC (overwritten by DemoAppView#12t12}
#vario t13 ╔═══ v12 DemoApp#11t10.loadUsers
#vario t13 ║ M DemoApp#11t13: users
#vario t13 ║ M Person#13t13: id, name, age, emails, log, _parent, _children
#vario t13 ║ M Person#14t13: id, name, age, emails, log, _parent, _children
#vario t13 ║ M Person#15t13: id, name, age, emails, log, _parent, _children
#vario t13 ║ M Person#16t13: id, name, age, emails, log, _parent, _children
#vario t13 ║ M Person#17t13: id, name, age, emails, log, _parent, _children
#vario t13 ║ M Person#18t13: id, name, age, emails, log, _parent, _children
#vario t13 ╚═══ v13 DemoApp#11t10.loadUsers - COMMIT(7)
#vario t13 gc t13 (DemoApp#11t10.loadUsers)
#vario t13 gc DemoApp#11t11 is ready for GC (overwritten by DemoApp#11t13}
#vario t13 gc Person#13t10 is ready for GC (overwritten by Person#13t13}
#vario t13 gc Person#14t10 is ready for GC (overwritten by Person#14t13}
#vario t13 gc Person#15t10 is ready for GC (overwritten by Person#15t13}
#vario t13 gc Person#16t10 is ready for GC (overwritten by Person#16t13}
#vario t13 gc Person#17t10 is ready for GC (overwritten by Person#17t13}
#vario t13 gc Person#18t10 is ready for GC (overwritten by Person#18t13}
#vario t14 ╔═══ v13 DemoAppView#12t10.autoprint
---
Filter: Jo
John's children: Billy, Barry, Steve
---
#vario t14 ║ M DemoAppView#12t14: filteredUsers, render, autoprint
#vario t14 ╚═══ v14 DemoAppView#12t10.autoprint - COMMIT(1)
#vario t14 ∞ DemoAppView#12t14.filteredUsers: #11t13.users, #12t14.userFilter, #16t13.name, #18t13.name
#vario t14 ∞ DemoAppView#12t14.render: #12t14.userFilter, #12t14.filteredUsers, #16t13._children, #13t13.name, #14t13.name, #15t13.name, #16t13.name
#vario t14 ∞ DemoAppView#12t14.autoprint: #12t14.render
#vario t14 gc t14 (DemoAppView#12t10.autoprint)
#vario t14 gc DemoAppView#12t12 is ready for GC (overwritten by DemoAppView#12t14}
#vario t15 ╔═══ v14 t1
#vario t15 ║ M Person#16t15: age, name, emails, _children
#vario t15 ║ M Person#13t15: name
#vario t15 ║ M Person#14t15: name
#vario t15 ║ M Person#15t15: name
#vario t15 ║ M DemoAppView#12t15: userFilter
#vario t15 ╚═══ v15 t1 - COMMIT(5)
#vario t15 x DemoAppView#12t14.filteredUsers is obsolete (by Person#16t15.name)
#vario t15 x DemoAppView#12t14.render is obsolete (by DemoAppView#12t14.filteredUsers)
#vario t15 x DemoAppView#12t14.autoprint is obsolete (by DemoAppView#12t14.render)
#vario t15 ■ DemoAppView#12t14.autoprint will be renewed automatically
#vario t15 gc t15 (t1)
#vario t15 gc Person#16t13 is ready for GC (overwritten by Person#16t15}
#vario t15 gc Person#13t13 is ready for GC (overwritten by Person#13t15}
#vario t15 gc Person#14t13 is ready for GC (overwritten by Person#14t15}
#vario t15 gc Person#15t13 is ready for GC (overwritten by Person#15t15}
#vario t15 gc DemoAppView#12t14 is ready for GC (overwritten by DemoAppView#12t15}
#vario t16 ╔═══ v15 DemoAppView#12t14.autoprint
---
Filter:
John Smith's children: Barry Smith, Steven Smith, William Smith
Kevin's children: Britney
---
#vario t16 ║ M DemoAppView#12t16: filteredUsers, render, autoprint
#vario t16 ╚═══ v16 DemoAppView#12t14.autoprint - COMMIT(1)
#vario t16 ∞ DemoAppView#12t16.filteredUsers: #11t13.users, #12t16.userFilter
#vario t16 ∞ DemoAppView#12t16.render: #12t16.userFilter, #12t16.filteredUsers, #16t15._children, #18t13._children, #13t15.name, #15t15.name, #14t15.name, #16t15.name, #17t13.name, #18t13.name
#vario t16 ∞ DemoAppView#12t16.autoprint: #12t16.render
#vario t16 gc t16 (DemoAppView#12t14.autoprint)
#vario t16 gc DemoAppView#12t15 is ready for GC (overwritten by DemoAppView#12t16}
Expected: Error: E609: object cannot be changed outside of transaction
#vario t17 ╔═══ v16 DemoAppView#12.dtor
#vario t17 ║ M DemoAppView#12t17: Symbol(dtor)
#vario t17 ╚═══ v17 DemoAppView#12.dtor - COMMIT(1)
#vario t17 x DemoAppView#12t16.filteredUsers is obsolete (by DemoAppView#12t17.userFilter)
#vario t17 x DemoAppView#12t16.render is obsolete (by DemoAppView#12t16.filteredUsers)
#vario t17 x DemoAppView#12t16.autoprint is obsolete (by DemoAppView#12t16.render)
#vario t17 gc t17 (DemoAppView#12.dtor)
#vario t17 gc DemoAppView#12t16 is ready for GC (overwritten by DemoAppView#12t17}
#vario t18 ╔═══ v17 DemoApp#11.dtor
#vario t18 ║ M DemoApp#11t18: Symbol(dtor)
#vario t18 ╚═══ v18 DemoApp#11.dtor - COMMIT(1)
#vario t18 gc t18 (DemoApp#11.dtor)
#vario t18 gc DemoApp#11t13 is ready for GC (overwritten by DemoApp#11t18}
*/
Async Demo
import { Vario, tran, cache } from "vario";
import { setTimeout } from "timers";
import fetch from "node-fetch";
@tran
export class DemoApp {
@tran title: string = "Demo";
@tran items: string[] = [];
@tran
async download(url: string, delay: number): Promise<void> {
this.title = "Demo (" + new Date().toISOString() + ")";
let start = Date.now();
await all([fetch(url), sleep(delay)]);
let ms = Date.now() - start;
this.items.push(`${url} in ${ms} ms`);
}
}
export class DemoAppView {
readonly model: DemoApp;
constructor(model: DemoApp) {
this.model = model;
}
@cache
async render(): Promise<string[]> {
let r: string[] = [];
r.push("---");
r.push("Title: " + this.model.title);
await sleep(1000);
r.push("Items: ");
for (let x of this.model.items)
r.push(" - " + x);
r.push("---");
return r;
}
@cache
async autoprint(): Promise<void> {
let lines: string[] = await this.render();
lines.forEach(x => console.log(x));
}
}
export async function sample(): Promise<void> {
let app = new DemoApp();
let view = new DemoAppView(app);
try {
Vario.autorenew(0, view.autoprint);
let list: Array<{ url: string, delay: number }> = [
{ url: "https://nezaboodka.com", delay: 700 },
{ url: "https://google.com", delay: 2000 },
{ url: "https://microsoft.com", delay: 700 },
];
await all(list.map(x => app.download(x.url, x.delay)));
Vario.autorenew(-1, view.autoprint);
}
catch (error) {
console.log(`${error}`);
}
finally {
Vario.dispose(view);
Vario.dispose(app);
}
}
async function sleep(timeout: number): Promise<void> {
return new Promise<void>(function(resolve) {
setTimeout(resolve.bind(null, () => resolve), timeout);
});
}
async function all(promises: Array<Promise<any>>): Promise<any[]> {
let error: any;
let result = await Promise.all(promises.map(x => x.catch(e => { error = error || e; return e; })));
if (error)
throw error;
return result;
}
/* Console output:
#vario t19 ╔═══ v18 DemoApp.ctor
#vario t19 ║ M DemoApp#19t19: title, items
#vario t19 ╚═══ v19 DemoApp.ctor - COMMIT(1)
#vario t19 gc t19 (DemoApp.ctor)
#vario t19 gc DemoApp#19t10 is ready for GC (overwritten by DemoApp#19t19}
#vario t20 ╔═══ v19 DemoAppView#20t10.autoprint
#vario t21 ╔═══ v19 DemoApp#19t10.download
#vario t22 ╔═══ v19 DemoApp#19t10.download
#vario t23 ╔═══ v19 DemoApp#19t10.download
#vario t21 ║ M DemoApp#19t21: title, items
#vario t21 ╚═══ v20 DemoApp#19t10.download - COMMIT(1)
#vario t23 ║ M DemoApp#19t23: title, items
#vario t23 ╚═══ v19 DemoApp#19t10.download - DISCARD(1) - Error: DemoApp#19t10.download conflicts with other transactions on: DemoApp#19t21.title, DemoApp#19t21.items
---
Title: Demo
Items:
---
#vario t20 ║ M DemoAppView#20t20: render, autoprint
#vario t20 ╚═══ v21 DemoAppView#20t10.autoprint - COMMIT(1)
#vario t20 ∞ DemoAppView#20t20.render: #19t19.title, #19t19.items
#vario t20 x DemoAppView#20t20.render is obsolete (by DemoApp#19t19.title)
#vario t20 ∞ DemoAppView#20t20.autoprint: #20t20.render
#vario t20 x DemoAppView#20t20.autoprint is obsolete (by DemoAppView#20t20.render)
#vario t20 ■ DemoAppView#20t20.autoprint will be renewed automatically
#vario t20 gc t20 (DemoAppView#20t10.autoprint)
#vario t20 gc DemoAppView#20t10 is ready for GC (overwritten by DemoAppView#20t20}
#vario t20 gc t21 (DemoApp#19t10.download)
#vario t20 gc DemoApp#19t19 is ready for GC (overwritten by DemoApp#19t21}
#vario t24 ╔═══ v21 DemoAppView#20t20.autoprint
#vario t22 ║ M DemoApp#19t22: title, items
#vario t22 ╚═══ v19 DemoApp#19t10.download - DISCARD(1) - Error: DemoApp#19t10.download conflicts with other transactions on: DemoApp#19t21.title, DemoApp#19t21.items
Error: DemoApp#19t10.download conflicts with other transactions on: DemoApp#19t21.title, DemoApp#19t21.items
#vario t25 ╔═══ v21 DemoAppView#20.dtor
#vario t25 ║ M DemoAppView#20t25: Symbol(dtor)
#vario t25 ╚═══ v22 DemoAppView#20.dtor - COMMIT(1)
#vario t26 ╔═══ v22 DemoApp#19.dtor
#vario t26 ║ M DemoApp#19t26: Symbol(dtor)
#vario t26 ╚═══ v23 DemoApp#19.dtor - COMMIT(1)
---
Title: Demo (2018-10-10T19:48:33.195Z)
Items:
- https://nezaboodka.com in 722 ms
---
#vario t24 ║ M DemoAppView#20t24: render, autoprint
#vario t24 ╚═══ v21 DemoAppView#20t20.autoprint - DISCARD(1) - Error: DemoAppView#20t20.autoprint conflicts with other transactions on: DemoAppView#20t25.render, DemoAppView#20t25.autoprint
#vario t24 gc t25 (DemoAppView#20.dtor)
#vario t24 gc DemoAppView#20t20 is ready for GC (overwritten by DemoAppView#20t25}
#vario t24 gc t26 (DemoApp#19.dtor)
#vario t24 gc DemoApp#19t21 is ready for GC (overwritten by DemoApp#19t26}
*/
API (TypeScript)
// Decorators
export type F<T> = (...args: any[]) => T;
export function tran(target: object, prop?: string, descriptor?: TypedPropertyDescriptor<F<any>>): any;
export function cache(target: Object, prop: string, descriptor: TypedPropertyDescriptor<F<any>>): any;
// Control functions
export interface Status { value: any; obsolete: boolean; error: any; latency: number; }
export class Vario {
statusof(method: F<any>): Status;
autorenew(latency: number, method: F<any>): void;
dispose(obj: object | undefined): void;
}
// Transaction
export class Transaction {
constructor(hint?: string);
run<T>(func: F<T>, ...args: any[]): T;
wrap<T>(func: F<T>): F<T>;
commit(): void;
sealToCommit(): Transaction; // t1.sealToCommit().waitForFinish().then(fulfill, reject)
discard(error?: any): Transaction; // t1.sealToCommit().waitForFinish().then(...)
waitForFinish(): Promise<void>;
finished(): boolean;
static run<T>(hint: string, func: F<T>, ...args: any[]): T;
static async runAsync<T>(hint: string, func: F<Promise<T>>, ...args: any[]): Promise<T>;
static get current(): Transaction;
}