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 simplify and improve productivity of Web UI development. Pushinka pushes state changes from state/model to corresponding UI elements for re-rendering in a seamless, consistent, real-time, and fine-grained way. This is achieved by two basic concepts: action and cache.
Action is a unit of work, possibly asynchronous, that makes data changes in an isolated data snapshot. The changes become visible only when action is succesfully completed.
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 used (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 action is successfully completed. With Pushinka you no longer need to create data change events in one objects, subscribe to these events in the other objects, and manually maintain switching from previous state to a new one.
Here is an example in TypeScript, which briefly illustrates the concept and its integration with React.
import { state, action, cache } from "pushinka";
import fetch from "node-fetch";
@state // treat all fields of the Model class as state
class Model {
url: string = "https://gitlab.com/nezaboodka/pushinka";
content: string = "Pushinka: Transactional, Reactive, and Asynchronous State Management";
timestamp: Date = Date.now();
@action // wrap method to execute it as an action (transactionally)
async load(url: string): Promise<void> {
// All the changes are made in a separate snapshot, which
// becomes visible only when action successfully completed.
this.url = url;
this.content = await fetch(url);
this.timestamp = Date.now();
} // action is completed, 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). To do so, separate data snapshot is automatically maintained for each action. The snapshot is logical and doesn't create full copy of all the data. Intermediate state is visible only inside action itself, but is not visible outside of the action until it is completed. Compensating actions are not needed in case of action failure, because all the changes made by action in its logical snapshot are simply discarded. In case action is successfully completed, affected cached values are invalidated and recomputed in a proper order at the end of the action (only when all data changes are confirmed).
Asynchronous operations (promises) are supported as first class citizens during action execution. Action may consist of a set of asynchronous operations that are confirmed on completion of all of them. Moreover, any asynchronous operation may spawn other asynchronous operations, which prolong action execution until the whole chain of asynchronous operations is fully completed. And in this case, caches are recomputed only at the end of entire action, thus preventing intermediate inconsistent state being leaked to UI.
Here is an example of integration of Pushinka and React:
import { cache, cacheof, dismiss } 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 (cacheof(this.render).isInvalidated)
this.forceUpdate();
}
componentDidMount(): void {
// Mark this.trigger to be recomputed automatically upon
// invalidation due to changes of its dependencies.
cacheof(this.trigger).mode = Recompute.Automatically;
this.trigger(); // first run to identify initial dependencies
}
shouldComponentUpdate(nextProps: Readonly<P>): boolean {
// Update component either if cache is invalidated or
// if props are different from the current ones.
let c = cacheof(this.render);
return c.isInvalidated || c.invalidateIf(diff(this.props, nextProps));
}
componentWillUnmount(): void {
dismiss(this); // cleanup
}
}Differentiators
- Simplicity, consistency and clarity are the first priorities
- Reactions, transactional actions, 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 action(target, prop, pd): any; // method only
export function separateAction(target, prop, pd): any; // method only
export function cache(target, prop, pd): any; // method only
// Control Functions
export function cacheof(method: Function): Cache;
export function dismiss(...objects: object[]): Action;
// Action
export type F<T> = (...args: any[]) => T;
export class Action {
constructor(hint: string);
run<T>(func: F<T>, ...args: any[]): T;
wrap<T>(func: F<T>): F<T>;
apply(): void;
seal(): Action; // t1.seal().whenFinished().then(fulfill, reject)
discard(error?: any): Action; // t1.seal().whenFinished().then(...)
finished(): boolean;
whenFinished(): Promise<void>;
static run<T>(func: F<T>, ...args: any[]): T;
static runAs<T>(hint: string, root: boolean, func: F<T>, ...args: any[]): T;
static async runAsync<T>(hint: string, func: F<Promise<T>>, ...args: any[]): Promise<T>;
static get current(): Action;
static debug: number = 0; // 0 = off, 1 = brief, 2 = normal, 3 = noisy, 4 = crazy
}
// Cache
export enum Recompute { Automatically = 0, Lazily = -1, Manually = -2 }
export abstract class Cache {
mode: Recompute;
latency: number;
monitor: Monitor | undefined;
readonly result: any;
readonly asyncResult: any;
readonly error: any;
readonly invalidator: string | undefined;
invalidateIf(invalidator: string | undefined): boolean;
readonly isInvalidated: boolean;
}
// Monitor
@state
export class Monitor {
readonly workers: number;
readonly idle: boolean;
constructor(name: string);
}