npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@ivy-js/trax

v1.0.0-alpha.3

Published

Trackable data objects

Downloads

5

Readme

Trax - Trackable Data Objects

Trax is a state management library that helps structuring and consuming data in typescript applications.

What is it for?

Most applications - in particular User Interfaces - use the MVC pattern to logically split the code in three entities: the data (aka. the Model - or state), the User Interface (aka. the View) and the actions (aka. the Controller).

These entities should ideally interact as follows:

  1. during an initialization phase, the view is built from the data objects
  2. on user interaction, the view triggers events that call actions
  3. actions change the data objects (synchronously or asynchronously)
  4. data changes trigger view updates to keep the view in sync with the new data ... and back to step #2

mv

In practice this sequence is not so simple to implement as data changes cannot be observed in JS applications. Besides, performance optimizations and scalability require to minimize the number of operations to update a view - so application developers (or UI frameworks) need to know precisely what particular piece of data has changed to produce efficient UIs.

This is where state management libraries step in as their goal is to offer the following possibilities:

  1. get notified when some data have changed
  2. know which part of the data have changed (and conversely which have not)
  3. ease conversion from/to JSON to store or retrieve data (from a server, a file or local storage)

Many popular state management libraries (such as redux, immerJS or immutableJS) have decided to use immutability as a way to solve problem #2 (if you are not familiar with immutability, it means that an object cannot be changed once created - so that applying changes to an object requires creating a new object instance (like for strings in JavaScript): so if data objects are immutable, they just need to be compared to know if they changed). The problem with immutability is that in a JS environment it imposes very painful, fragile and heavy coding patterns.

This is why the core idea behind trax is to use a versioning system instead of immutability.

How does it work?

Here is how the previous MVC sequence is executed in a trax environment (note: more optimized versions can be implemented, but this to give the general idea).

  1. during the initialization phase
    • the view is built from the data objects
    • each view fragment memorizes the version number(s) of the data object(s) it depends on
    • the view asks to be notified in case of data change (i.e. a watcher callback is registered)
  2. on user interaction, the view triggers events that call actions
  3. actions change the data objects (synchronously or asynchronously). In a trax environment, actions are normal JS functions that create, update or delete data objects as any normal JS objects. Actions don't even need to know that the data objects are trax objects as they don't use any particular setter or getter APIs (like setState in React applications). Behind the scene, here is what trax is doing:
    • trax creates hidden property setters and property getters and / or proxy objects at compilation time to track updates made on trax objects.
    • trax also associates an internal version number to each trax object (aka. data object)
    • anytime a data object is changed, trax updates its internal version number:
      • if the version number was even (e.g. 2), it is incremented (e.g. to 3). Even numbers denote objects that are clean.
      • if the version number was odd (e.g. 3), it remains unchanged. Odd numbers denote objects that are dirty
      • all parent objects of the changed object are marked as dirty as well (also by incrementing their version number, if need be)
    • on first data change trax triggers an asynchronous micro task to clean all dirty objects when the JS execution is complete
    • when all JS actions are executed, the JS thread executes the micro task and performs the following actions:
      • dirty objects version numbers are incremented (to make them clean again)
      • all associated watcher callbacks are called
  4. watcher callbacks trigger View refresh
    • view entities compare the version number of the new data with the previous memorized version numbers (cf. step #1)
    • if version differs, the new version is memorized and the associated view fragment is recalculated

Usage

The previous explanation may give the impression that using trax is complex. It is actually the opposite. From the developer's point of view, here is what it looks like.

First, the trax objects have to be defined. Let's imagine for instance that you want to model a list of 'todo' tasks - here is what you would need to write:

import { Data } from 'trax';

@Data class TodoTask {
    description = "";
    done = false;
}

@Data class TodoList {
    name: string;
    tasks: TodoTask[];
}

As you can see, defining trax objects consists in declaring value object classes annotated with the @Data decorator. This decorator is used at build time by trax to rewrite the class code and add meta-data information (trax provides rollup and webpack plugins for this).

Then you will need to instantiate your data objects. This can be done either manually:

const ls = new TaskList();
ls.name = "dev todos";

let t = new TodoTask();
t.description = "assess trax";
ls.push(t);

or by creating the data object from a JSON object:

import { create } from 'trax';
const ls = create(TaskList, {name: "dev todos", tasks:[{ description:"assess trax" }]});

At this point, the ls object is created and can be consumed immediately.

As an example, let's imagine that we want do display the task list in the console:

function render(tl: TaskList) {
    console.log(`${tl.name}:`);
    tl.tasks.forEach((task:TodoTask, idx:number) => {
        console.log(`${idx+1}. ${task.description} ${task.done? '(done!)' : ''}`);
    });
}
render(ls);

The console output should look like this:

dev todos:
1. assess trax

Now, if you want the console to keep constantly in sync with the ls data, you simply need to watch the ls instance and call render again anytime a change is reported:

import { watch } from 'trax';
watch(ls, () => render(ls)); // render will be called anytime a change occurs in ls or its children

After that, running code like this:

ls.tasks[0].done = true;

will automatically trigger an asynchronous render call that will be displayed in the console:

dev todos:
1. assess trax (done!)

As the watchers are called asynchronously (through the micro task), multiple synchronous changes will result in only one watcher call:

// the following set of operations will only trigger one watcher call
ls.tasks.push(create(TodoTask, {description: "check micro-tasks"}));
ls.tasks.name = "todo - important";

Note: a more complete example based on the TodoMVC application is available here

Benefits

  • extremely simple - actually almost transparent compared to other solutions
  • typescript support (type validation & IDE auto-completion)
  • from / to JSON conversion
  • plugins for rollup and webpack
  • tree-shaking support
  • small: from ~5 to ~9kb gzipped (size depends on usage - cf. tree-shakability)
  • and of course works with any UI layer

Use cases

  • Web User Interfaces rendering optimization
  • State persistence: cf. convertToJson and create apis
  • Asynchronous UIs: data can be changed from anywhere, anytime, independently form user events
  • Modular client architecture with shared data stores
  • Undo/redo: a data object can be watched and previous versions can be stored / re-hydrated through convertToJson & create apis

Full api documentation here

Syntax cheat-sheet here