0.7.0 • Published 6 years ago

pushka v0.7.0

Weekly downloads
2
License
MIT
Repository
github
Last release
6 years ago

Pushka • Transactional, Reactive, and Asynchronous State Management for JavaScript

Inspired by: MobX, Nezaboodka, React, Excel.

Introduction

Pushka 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). Pushka 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 just about 1K lines of code

Demo

import { Pushka, Transaction, tran, cache } from "pushka";
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: ["john@mail.com"],
      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: ["kevin@mail.com"],
      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();
    Pushka.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] = "daddy@mail.com";
      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("dad@mail.com");
      else
        daddy.children[1].name = "Billy Smithy";
    }
    catch (e) {
      console.log(`Expected: ${e}`);
    }
    // Turn off auto renew
    Pushka.autorenew(-1, view.autoprint);
  }
  finally { // cleanup
    Pushka.dispose(view);
    Pushka.dispose(app);
  }
}

/* Console output:

#pushka t11 ╔═══ v10 DemoApp.ctor
#pushka t11 ║ M DemoApp#11t11: title, users
#pushka t11 ╚═══ v11 DemoApp.ctor - COMMIT(1)
#pushka t11  gc t11 (DemoApp.ctor)
#pushka t11  gc DemoApp#11t10 is ready for GC (overwritten by DemoApp#11t11}
#pushka t12 ╔═══ v11 DemoAppView.ctor
#pushka t12 ║ M DemoAppView#12t12: userFilter
#pushka t12 ╚═══ v12 DemoAppView.ctor - COMMIT(1)
#pushka t12  gc t12 (DemoAppView.ctor)
#pushka t12  gc DemoAppView#12t10 is ready for GC (overwritten by DemoAppView#12t12}
#pushka t13 ╔═══ v12 DemoApp#11t10.loadUsers
#pushka t13 ║ M DemoApp#11t13: users
#pushka t13 ║ M Person#13t13: id, name, age, emails, log, _parent, _children
#pushka t13 ║ M Person#14t13: id, name, age, emails, log, _parent, _children
#pushka t13 ║ M Person#15t13: id, name, age, emails, log, _parent, _children
#pushka t13 ║ M Person#16t13: id, name, age, emails, log, _parent, _children
#pushka t13 ║ M Person#17t13: id, name, age, emails, log, _parent, _children
#pushka t13 ║ M Person#18t13: id, name, age, emails, log, _parent, _children
#pushka t13 ╚═══ v13 DemoApp#11t10.loadUsers - COMMIT(7)
#pushka t13  gc t13 (DemoApp#11t10.loadUsers)
#pushka t13  gc DemoApp#11t11 is ready for GC (overwritten by DemoApp#11t13}
#pushka t13  gc Person#13t10 is ready for GC (overwritten by Person#13t13}
#pushka t13  gc Person#14t10 is ready for GC (overwritten by Person#14t13}
#pushka t13  gc Person#15t10 is ready for GC (overwritten by Person#15t13}
#pushka t13  gc Person#16t10 is ready for GC (overwritten by Person#16t13}
#pushka t13  gc Person#17t10 is ready for GC (overwritten by Person#17t13}
#pushka t13  gc Person#18t10 is ready for GC (overwritten by Person#18t13}
#pushka t14 ╔═══ v13 DemoAppView#12t10.autoprint
---
Filter: Jo
John's children: Billy, Barry, Steve
---
#pushka t14 ║ M DemoAppView#12t14: filteredUsers, render, autoprint
#pushka t14 ╚═══ v14 DemoAppView#12t10.autoprint - COMMIT(1)
#pushka t14   ∞ DemoAppView#12t14.filteredUsers: #11t13.users, #12t14.userFilter, #16t13.name, #18t13.name
#pushka t14   ∞ DemoAppView#12t14.render: #12t14.userFilter, #12t14.filteredUsers, #16t13._children, #13t13.name, #14t13.name, #15t13.name, #16t13.name
#pushka t14   ∞ DemoAppView#12t14.autoprint: #12t14.render
#pushka t14  gc t14 (DemoAppView#12t10.autoprint)
#pushka t14  gc DemoAppView#12t12 is ready for GC (overwritten by DemoAppView#12t14}
#pushka t15 ╔═══ v14 t1
#pushka t15 ║ M Person#16t15: age, name, emails, _children
#pushka t15 ║ M Person#13t15: name
#pushka t15 ║ M Person#14t15: name
#pushka t15 ║ M Person#15t15: name
#pushka t15 ║ M DemoAppView#12t15: userFilter
#pushka t15 ╚═══ v15 t1 - COMMIT(5)
#pushka t15   x DemoAppView#12t14.filteredUsers is obsolete (by Person#16t15.name)
#pushka t15   x DemoAppView#12t14.render is obsolete (by DemoAppView#12t14.filteredUsers)
#pushka t15   x DemoAppView#12t14.autoprint is obsolete (by DemoAppView#12t14.render)
#pushka t15   ■ DemoAppView#12t14.autoprint will be renewed automatically
#pushka t15  gc t15 (t1)
#pushka t15  gc Person#16t13 is ready for GC (overwritten by Person#16t15}
#pushka t15  gc Person#13t13 is ready for GC (overwritten by Person#13t15}
#pushka t15  gc Person#14t13 is ready for GC (overwritten by Person#14t15}
#pushka t15  gc Person#15t13 is ready for GC (overwritten by Person#15t15}
#pushka t15  gc DemoAppView#12t14 is ready for GC (overwritten by DemoAppView#12t15}
#pushka t16 ╔═══ v15 DemoAppView#12t14.autoprint
---
Filter:
John Smith's children: Barry Smith, Steven Smith, William Smith
Kevin's children: Britney
---
#pushka t16 ║ M DemoAppView#12t16: filteredUsers, render, autoprint
#pushka t16 ╚═══ v16 DemoAppView#12t14.autoprint - COMMIT(1)
#pushka t16   ∞ DemoAppView#12t16.filteredUsers: #11t13.users, #12t16.userFilter
#pushka t16   ∞ DemoAppView#12t16.render: #12t16.userFilter, #12t16.filteredUsers, #16t15._children, #18t13._children, #13t15.name, #15t15.name, #14t15.name, #16t15.name, #17t13.name, #18t13.name
#pushka t16   ∞ DemoAppView#12t16.autoprint: #12t16.render
#pushka t16  gc t16 (DemoAppView#12t14.autoprint)
#pushka t16  gc DemoAppView#12t15 is ready for GC (overwritten by DemoAppView#12t16}
Expected: Error: E609: object cannot be changed outside of transaction
#pushka t17 ╔═══ v16 DemoAppView#12.dtor
#pushka t17 ║ M DemoAppView#12t17: Symbol(dtor)
#pushka t17 ╚═══ v17 DemoAppView#12.dtor - COMMIT(1)
#pushka t17   x DemoAppView#12t16.filteredUsers is obsolete (by DemoAppView#12t17.userFilter)
#pushka t17   x DemoAppView#12t16.render is obsolete (by DemoAppView#12t16.filteredUsers)
#pushka t17   x DemoAppView#12t16.autoprint is obsolete (by DemoAppView#12t16.render)
#pushka t17  gc t17 (DemoAppView#12.dtor)
#pushka t17  gc DemoAppView#12t16 is ready for GC (overwritten by DemoAppView#12t17}
#pushka t18 ╔═══ v17 DemoApp#11.dtor
#pushka t18 ║ M DemoApp#11t18: Symbol(dtor)
#pushka t18 ╚═══ v18 DemoApp#11.dtor - COMMIT(1)
#pushka t18  gc t18 (DemoApp#11.dtor)
#pushka t18  gc DemoApp#11t13 is ready for GC (overwritten by DemoApp#11t18}

*/

Async Demo

import { Pushka, tran, cache } from "pushka";
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 {
    Pushka.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)));
    Pushka.autorenew(-1, view.autoprint);
  }
  catch (error) {
    console.log(`${error}`);
  }
  finally {
    Pushka.dispose(view);
    Pushka.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:

#pushka t19 ╔═══ v18 DemoApp.ctor
#pushka t19 ║ M DemoApp#19t19: title, items
#pushka t19 ╚═══ v19 DemoApp.ctor - COMMIT(1)
#pushka t19  gc t19 (DemoApp.ctor)
#pushka t19  gc DemoApp#19t10 is ready for GC (overwritten by DemoApp#19t19}
#pushka t20 ╔═══ v19 DemoAppView#20t10.autoprint
#pushka t21 ╔═══ v19 DemoApp#19t10.download
#pushka t22 ╔═══ v19 DemoApp#19t10.download
#pushka t23 ╔═══ v19 DemoApp#19t10.download
#pushka t21 ║ M DemoApp#19t21: title, items
#pushka t21 ╚═══ v20 DemoApp#19t10.download - COMMIT(1)
#pushka t23 ║ M DemoApp#19t23: title, items
#pushka 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:
---
#pushka t20 ║ M DemoAppView#20t20: render, autoprint
#pushka t20 ╚═══ v21 DemoAppView#20t10.autoprint - COMMIT(1)
#pushka t20   ∞ DemoAppView#20t20.render: #19t19.title, #19t19.items
#pushka t20   x DemoAppView#20t20.render is obsolete (by DemoApp#19t19.title)
#pushka t20   ∞ DemoAppView#20t20.autoprint: #20t20.render
#pushka t20   x DemoAppView#20t20.autoprint is obsolete (by DemoAppView#20t20.render)
#pushka t20   ■ DemoAppView#20t20.autoprint will be renewed automatically
#pushka t20  gc t20 (DemoAppView#20t10.autoprint)
#pushka t20  gc DemoAppView#20t10 is ready for GC (overwritten by DemoAppView#20t20}
#pushka t20  gc t21 (DemoApp#19t10.download)
#pushka t20  gc DemoApp#19t19 is ready for GC (overwritten by DemoApp#19t21}
#pushka t24 ╔═══ v21 DemoAppView#20t20.autoprint
#pushka t22 ║ M DemoApp#19t22: title, items
#pushka 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
#pushka t25 ╔═══ v21 DemoAppView#20.dtor
#pushka t25 ║ M DemoAppView#20t25: Symbol(dtor)
#pushka t25 ╚═══ v22 DemoAppView#20.dtor - COMMIT(1)
#pushka t26 ╔═══ v22 DemoApp#19.dtor
#pushka t26 ║ M DemoApp#19t26: Symbol(dtor)
#pushka t26 ╚═══ v23 DemoApp#19.dtor - COMMIT(1)
---
Title: Demo (2018-10-10T19:48:33.195Z)
Items:
 - https://nezaboodka.com in 722 ms
---
#pushka t24 ║ M DemoAppView#20t24: render, autoprint
#pushka t24 ╚═══ v21 DemoAppView#20t20.autoprint - DISCARD(1) - Error: DemoAppView#20t20.autoprint conflicts with other transactions on: DemoAppView#20t25.render, DemoAppView#20t25.autoprint
#pushka t24  gc t25 (DemoAppView#20.dtor)
#pushka t24  gc DemoAppView#20t20 is ready for GC (overwritten by DemoAppView#20t25}
#pushka t24  gc t26 (DemoApp#19.dtor)
#pushka 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 Pushka {
  statusof(method: F<any>): Status;
  autorenew(latency: number, method: F<any>, ...args: 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;
}