Package Exports
- pushinka
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (pushinka) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
Pushinka: Reactive, Transactional, and Asynchronous State Management
About
Open source project of Nezaboodka Software.
Demo: https://nezaboodka.gitlab.io/pushinka-demo
Inspired by: MobX, Nezaboodka, Excel.
Pushinka is a JavaScript state management library, which combines the power of reactive, transactional, and asynchronous programming models to radically improve productivity of Web UI development. It pushes state changes from data storage to corresponding UI elements for re-rendering in a seamless, consistent, real-time, and fine-grained way. This is achieved by two basic concepts: transaction and cache.
Transaction is a unit of work, possibly asynchronous, that makes data changes in an isolated data snapshot. The changes become visible only when transaction is committed.
Cache is a result of a function that is remembered and used to avoid redundant function calls. The cached value is automatically invalidated and recomputed upon changes in any data that were accessed by the function.
Pushinka takes full care of tracking dependencies between accessed data (observables) and dependant cached values (observers), and provides fine-grained immediate or lazy recomputation of cached values. Recomputations are fully consistent and take place only when transaction is committed. With Pushinka you no longer need to create data change events in one objects and subscribe to these events in the other objects.
Here is an example in TypeScript, which briefly illustrates the concept and its integration with React.
import { state, transaction, cache } from "pushinka";
import fetch from 'node-fetch';
@state // treat all fields of the Model class as transactional
class Model {
url: string = "https://gitlab.com/nezaboodka/pushinka";
content: string = "Pushinka: Transactional, Reactive, and Asynchronous State Management";
timestamp: Date = Date.now();
@transaction // wrap method to execute it as a transaction
async load(url: string): Promise<void> {
// All the changes are made in a separate snapshot,
// which becomes visible on transaction commit.
this.url = url;
this.content = await fetch(url);
this.timestamp = Date.now();
} // transaction is committed, dependant caches are notified and recomputed
}
class View extends PushinkaReactComponent<Model> { // see PushinkaReactComponent below
@cache // wrap method to cache its result and to track its dependencies
render(): ReactNode {
const m: Model = this.props; // just a shortcut
return (
<div>
<h1>{m.url}</h2>
<div>{m.content}</div>
</div>
);
// Render cache is subscribed to m.url and m.content, but not m.timestamp
}
}In the example above the result of the method render is cached and reused by React rendering system to avoid redundant generation of the target HTML. When the title and content fields of the data model are changed (by some other code), the existing cached HTML is invalidated and then automatically recomputed by re-executing the render method.
Pushinka automatically recomputes cached functions upon changes of object properties that were accessed during function execution. To do so, Pushinka intercepts property getters and setters of the accessed JavaScript objects. Property getters are used to track what properties (observables) are used by a given cached function (observer). When some of the properties are changed, the corresponding cached function is automatically re-executed.
Multiple object properties can be changed in a transactional way - all at once with full respect to the all-or-nothing principle (atomicity, consistency, and isolation). The separate data snapshot is automatically maintained 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. In case of successful transaction, affected cached values are invalidated and recomputed in a proper order at the end of transaction (only when all data changes are committed).
Asynchronous operations (promises) are supported as first class citizens during transaction processing. Transaction may consist of a set of asynchronous operations that are committed on completion of all of them. Moreover, any asynchronous operation may spawn other asynchronous operations, which prolong transaction execution until the whole chain of asynchronous operations is fully completed. And in this case, caches are recomputed only at the end of entire transaction, thus preventing intermediate inconsistent state being leaked to UI.
Here is an example of integration of Pushinka and React:
import { Pushinka, cache } from "pushinka";
import * as React from 'react';
class PushinkaReactComponent<P> extends React.Component<P> {
@cache
trigger(): void {
// This method is automatically re-executed
// when cached result of this.render is invalidated.
if (this.render.isInvalidated)
this.forceUpdate();
}
componentDidMount(): void {
// Run this.trigger to identify initial dependencies.
// And mark it to re-run automatically upon changes
// in its dependencies.
this.trigger();
this.trigger.mode = Recompute.Automatically;
}
shouldComponentUpdate(nextProps: Readonly<P>): boolean {
return this.render.isInvalidated;
}
componentWillUnmount(): void {
Pushinka.dismiss(this); // deactive all caches
}
}Differentiators
- Simplicity, consistency and clarity are the first priorities
- Reactive, transactional, and asynchronous operations are first-class citizens
- Undo/redo functionality is built-in and provided out of the box
- It's minimalistic library, not a framework
API (TypeScript)
// Decorators
export function state(target, prop?): any; // class or field
export function raw(target, prop): any; // field only
export function transaction(target, prop, pd): any; // method only
export function separateTransaction(target, prop, pd): any; // method only
export function cache(target, prop, pd): any; // method only
// Pushinka control functions
export type F<T> = (...args: any[]) => T;
export enum Recompute { Automatically = 0, Lazily = -1, Manually = -2 }
export class Pushinka {
mode(method: F<any>): Recompute;
setMode(method: F<any>, value: Recompute): void;
latency(method: F<any>): number;
setLatency(method: F<any>, value: number): void;
isRunning(method: F<any>): boolean;
result(method: F<any>): any;
asyncResult(method: F<any>): any;
error(method: F<any>): any;
isInvalidated(method: F<any>): boolean;
invalidate(method: F<any>): void;
static dismiss(...objects: object[]): void;
static logging: boolean;
static tracing: boolean;
}
declare global {
interface Function {
mode: Recompute; // Pushinka.mode/setMode
latency: number; // Pushinka.latency/setLatency
readonly isRunning: boolean; // Pushinka.isRunning
readonly result: any; // Pushinka.result
readonly asyncResult: any; // Pushinka.asyncResult
readonly error: any; // Pushinka.error
readonly isInvalidated: boolean; // Pushinka.isInvalidated
}
}
// Transaction
export class Transaction {
constructor(hint: string);
run<T>(func: F<T>, ...args: any[]): T;
wrap<T>(func: F<T>): F<T>;
commit(): void;
seal(): Transaction; // t1.seal().whenFinished().then(fulfill, reject)
discard(error?: any): Transaction; // t1.seal().whenFinished().then(...)
finished(): boolean;
whenFinished(): Promise<void>;
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;
}