Skip to content

A runtime extension to JavaScript that unlocks Imperative Reactive Programming (IRP) in JavaScript!

License

Notifications You must be signed in to change notification settings

webqit/use-live

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

UseLive — Live Mode for JavaScript

npm version bundle License

OverviewCreating Live ProgramsImplementationExamplesLicense

UseLive ("use live") is a runtime extension to JavaScript that enables live execution mode in JavaScript.

What's that?

Overview

Where you normally would require certain reactive primitives to express reactive logic...

// Import reactive primitives
import { createSignal, createMemo, createEffect } from "solid-js";

// Declare values
const [count, setCount] = createSignal(5);
const doubleCount = createMemo(() => count() * 2);
// Log this value live
createEffect(() => {
  console.log(doubleCount());
});
// Setup periodic updates
setInterval(() => setCount(10), 1000);

The "use live" directive gives you same reactive behavior on top of your ordinary imperative code:

"use live";
// Declare values
let count = 5;
let doubleCount = count * 2;
// Log this value live
console.log(doubleCount);
// Setup periodic updates
setInterval(() => {
  "use live";
  count = 10;
}, 1000);

To try:

  1. Add the following script to your page:
<script src="https://unpkg.com/@webqit/oohtml/dist/main.js"></script>
  1. Write your logic with a use live directive:
<script>
  "use live";
  // Declare values
  let count = 5;
  let doubleCount = count * 2;
  // Log this value live
  console.log(doubleCount);

  // Setup periodic updates
  setInterval(() => {
    "use live";
    count = 10;
  }, 1000);
</script>
  1. Watch your console.

To go one step further, update your step 2 to split the logic into two separate scripts:

2.:

<script>
  "use live";
  // Declare values
  let count = 5;
  let doubleCount = count * 2;
  // Setup periodic updates
  setInterval(() => {
    "use live";
    count = 10;
  }, 1000);
</script>
<script>
  "use live";
  // Log this value live
  console.log(doubleCount);
</script>

Watch your console. Reactivity should still work.

To define, live programs are JavaScript programs that stay sensitive to changes in program state in fine-grained details - and with no moving parts.

While that is the <script>"use live"</script> part of the HTML page above, there are many different forms of live programs. Examples are just ahead.

Note

This project pursues a futuristic, more efficient way to write reactive applocations. And it occupies a new category in the reactivity spectrum.

Note

You’re viewing @webqit/use-live — the newest iteration.
For the prev 4.6.x branch, see webqit/[email protected].*.

Implementation

As seen above, UseLive can run in the browser.

Up there, we've used a version of the UseLive implementation that supports HTML <script> elements. UseLive works in HTML via the OOHTML project (OOHTML) and it's the most direct way to use UseLive in the browser.

That said, UseLive is directly usable in many different ways — both in the browser and in Node.js.

UseLive in the browser

Load from a CDN
└─────────
<script src="https://unpkg.com/@webqit/use-live/dist/main.js"></script>

└ This is to be placed early on in the document and should be a classic script without any defer or async directives.

// Destructure from the webqit namespace
const {
  LiveFunction,
  AsyncLiveFunction,
  LiveScript,
  LiveModule,
  LiveMode,
  Observer,
} = window.webqit;

UseLive in Node.js

Install from NPM
└─────────
// npm install
npm i @webqit/use-live
// Import API
import {
  LiveFunction,
  AsyncLiveFunction,
  LiveScript,
  AsyncLiveScript,
  LiveModule,
  LiveMode,
  Observer,
} from "@webqit/use-live";

UseLive Lite

It is possible to use a lighter version of UseLive where you want something further feather weight for your initial application load.

Load from a CDN
└─────────
<script src="https://unpkg.com/@webqit/use-live/dist/main.lite.js"></script>

└ This is to be placed early on in the document and should be a classic script without any defer or async directives!

// Destructure from the webqit namespace
const { AsyncLiveFunction, AsyncLiveScript, LiveModule, LiveMode, Observer } =
  window.webqit;
Additional details

The Lite APIs initially come without the compiler and yet lets you work with UseLive ahead of that. Additionally, these APIs are able to do their compilation off the main thread by getting the UseLive compiler loaded into a Web Worker!

But if you may, the UseLive Compiler is all still loadable directly - as if short-circuiting the lazy-loading strategy of the Lite APIs:

<head>
  <script src="https://unpkg.com/@webqit/use-live/dist/compiler.js"></script>
  <!-- Must come before the polyfil -->
  <script src="https://unpkg.com/@webqit/use-live/dist/main.lite.js"></script>
</head>
Install from NPM
└─────────
// npm install
npm i @webqit/use-live
// Import Lite API
import {
  AsyncLiveFunction,
  AsyncLiveScript,
  LiveModule,
  LiveMode,
  Observer,
} from "@webqit/use-live/lite";

Creating Live Programs

Live Functions

You declare live functions by adding the "use live" directive as first statement in the function body. And you can also use the LiveFunction and AsyncLiveFunction APIs. (The first option requires a compile step, the second doesn't.)

The "use live" Directive (Option 1)

Function Declarations

function bar() {
  "use live";
  let count = 5;
  let doubleCount = count * 2;
  console.log(doubleCount);
}
bar();
async function bar() {
  "use live";
  let count = await 5;
  let doubleCount = count * 2;
  console.log(doubleCount);
}
await bar();
Show more syntax examples

Function Expressions

const bar = function () {
  "use live";
};
const bar = async function () {
  "use live";
};

Object Properties

const foo = {
  bar: function () {
    "use live";
  },
};
const foo = {
  bar: async function () {
    "use live";
  },
};

Object Methods

const foo = {
  bar() {
    "use live";
  },
};
const foo = {
  async bar() {
    "use live";
  },
};

Class Methods

class Foo {
  bar() {
    "use live";
  },
}
class Foo {
  async bar() {
    "use live";
  },
}

Arrow Functions

const bar = () => {
  "use live";
};
const bar = async () => {
  "use live";
};

If you have a build process for your app, you can use the UseLive compiler to compile your live functions. It's easy:

import { compile } from "@webqit/use-live";

const inputSource = `
    function bar() {
      "use live";
      let count = 5;
      let doubleCount = count * 2;
      console.log(doubleCount);
    }
`;

const resultString = compile('function', inputSource);

Or to compile files conditionally:

import { parse, compile } from "@webqit/use-live";

const ast = parse(inputSource);
if (ast.isLiveProgram || ast.hasLiveFunctions) {
    const resultString = compile('function', ast);
}

Plugins for bundlers like esbuild and rollup are available soon.

Live Function Constructors (Option 2)

UseLive has the concept of function constructors that allow you to create functions at runtime — without a build step. You obtain the APIs as shown below:

// Import API
import {
  LiveFunction,
  AsyncLiveFunction,
} from "@webqit/use-live";

or, if loaded in the browser:

// Destructure from the webqit namespace
const {
  LiveFunction,
  AsyncLiveFunction,
} = window.webqit;

Here, LiveFunction and AsyncLiveFunction give you the equivalent of function() {} and async function() {} respectively.

const bar = LiveFunction(`
  let count = 5;
  let doubleCount = count * 2;
  console.log(doubleCount);
`);
bar();
const bar = AsyncLiveFunction(`
  let count = await 5;
  let doubleCount = count * 2;
  console.log(doubleCount);
`);
await bar();
Show more syntax examples
// With function parameters
const bar = LiveFunction(param1, ...paramN, functionBody);
// With the new keyword
const bar = new LiveFunction(param1, ...paramN, functionBody);
// As class property
class Foo {
  bar = LiveFunction(param1, ...paramN, functionBody);
}

Global variables are accessible in the body of these constructed functions.

// External dependency
globalThis.externalVar = 10;

// LiveFunction
const sum = LiveFunction(`a`, `b`, `return a + b + externalVar;`);
const state = sum(10, 10);

// Inspect
console.log(state.value); // 30

And note that as is the noraml behaviour of function constructors in JavaScript, only the global scope is accessible as shown. Variables in the surrounding scope are not accessible:

let a;
globalThis.b = 2;
var c = "c"; // Equivalent to globalThis.c = 'c' assuming that we aren't running in a function scope or module scope

const bar = LiveFunction(`
  console.log(typeof a); // undefined
  console.log(typeof b); // number
  console.log(typeof c); // string
`);
bar();

Note that, unlike the main UseLive build, the UseLive Lite edition only implements the AsyncLiveFunction API.

Live Scripts (Whole Programs)

UseLive has the concept of whole scripts as live programs — whether classic scripts or module scripts. You use either the "use live" directive (which requires a compile step) or the LiveScript and LiveModule APIs (which don't).

The "use live" Directive (Option 1)

// script: index.js
"use live";
globalThis.count = 0;
setInterval(() => {
  count++;
}, 1000);
// module: index.js
"use live";
// Import dependencies where needed
import module1, { module2 } from 'package-name';

// Export a live variable
export let count = 0;

// Update the variable every second
setInterval(() => {
  count++;
}, 1000);

As in the case of live functions above, these can be compiled as part of your application's build process. (In the compilation example above, you'll get ast.isLiveProgram === true for these scripts.)

The LiveScript and LiveModule APIs (Option 2)

These APIs give you the equivalent of <script> and <script type="module"> respectively. You obtain the APIs as shown below:

// Import API
import {
  LiveModule,
  LiveScript,
  AsyncLiveScript,
} from "@webqit/use-live";

Or, if loaded in the browser:

const {
  LiveModule,
  LiveScript,
  AsyncLiveScript,
} = window.webqit;

Here, LiveModule, LiveScript, and AsyncLiveScript give you the equivalent of <script type="module">, <script>, and <script async> respectively.

// Live module
const program = new LiveModule(`
  // Import dependencies where needed
  import module1, { module2 } from 'package-name';

  // Export a live variable
  export let count = 0;

  // Update the variable every second
  setInterval(() => {
    count++;
  }, 1000);
`);
await program.execute();
const program = new LiveScript(`
  globalThis.count = 0;
  setInterval(() => {
    count++;
  }, 1000);
`);
program.execute();

Note that, unlike the main UseLive build, the UseLive Lite edition only implements the AsyncLiveScript and LiveModule APIs.

Consuming Live Programs

Each call to a Live function or script returns back a LiveMode object that lets you access values exposed by the program.

For Live functions:

const liveMode = bar();

For Live scripts:

const liveMode = program.execute();

For Live HTML scripts - <script>"use live"</script>, the liveMode object is available as a direct property of the script element:

console.log(script.liveMode);

Return Value

For functions, the LiveMode object exposes a value property that carries the program's actual return value:

function sum(a, b) {
  "use live";
  return a + b;
}
const liveMode = sum(5, 4);
console.log(liveMode.value); // 9

But given the concept of "live", liveMode.value is a "live" property that always reflects the program's return value at any point in time:

function counter() {
  "use live";
  let count = 0
  setInterval(() => count++, 500);
  return count;
}
const liveMode = counter();
console.log(liveMode.value); // 0

The general-purpose, object-observability API: Observer API may be used to observe changes to the value property:

Observer.observe(liveMode, "value", (mutation) => {
  //console.log(liveMode.value); Or:
  console.log(mutation.value); // 1, 2, 3, 4, etc.
});

Module Exports

For module programs, the LiveMode object exposes an exports property that produces the module's exports:

const liveMode = await program.execute();
console.log(liveMode.exports); // { count }

But given the concept of "live", each property in the liveMode.exports object is a "live" property that always reflects an export's internal value at any point in time:

const program = new LiveModule(`
  export let localVar = 0;
  ...
  setInterval(() => localVar++, 500);
`);
const state = await program.execute();
console.log(state.exports); // { localVar }

Again, the Observer API puts those changes in your hands:

Observer.observe(state.exports, 'localVar', (mutation) => {
  console.log(mutation.value); // 1, 2, 3, 4, etc.
});
// Observe "any" export
Observer.observe(state.exports, (mutations) => {
  mutations.forEach((mutation) => console.log(mutation.key, mutation.value));
});

Aborting Live Programs

Live programs may maintain many live relationships and should be aborted when their work is done! The LiveMode object they return exposes an abort() method that lets us do that:

liveMode.abort();

For Live HTML Scripts - <script>"use live"</script>, this cleanup is automatic as script element leaves the DOM!

Interaction with the Outside World

Live programs can read and write to the given scope in which they run; just in how a regular JavaScript function can reference outside variables and also make side effects:

let a = 2, b;
function bar() {
  "use live";
  b = a * 2;
  console.log('Total:', b);
}
bar();

But as an extension to regular JavaScript, Live programs maintain a live relationship with the outside world! This means that:

...Updates Happening On the Outside Are Automatically Reflected

Given the code above, the following will now be reflected:

// Update external dependency
a = 4;

The above dependency and reactivity hold the same even if you had a in the place of a parameter's default value:

let a = 2, b = 0;
function bar(param = a) {
  "use live";
  b = param * 2;
  console.log('Total:', b);
}
bar();

And you get the same automatic dependency tracking with object properties:

// External value
const obj = { a: 2, b: 0 };
function bar() {
  "use live";
  obj.b = obj.a * 2;
  console.log('Total:', obj.b);
}
bar();
// Update external dependency
obj.a = 4;

...Updates Happening On the Inside Are Observable

Given the same data binding principles, you are able to observe updates the other way round as to the updates made from the inside of the function: b = 4, obj.b = 4!

For updates to object properties, you use the Observer API directly:

// Observe changes to object properties
const obj = { a: 2, b: 0 };
Observer.observe(obj, "b", (mutation) => {
  console.log("New value:", mutation.value);
});

The above also holds for global variables:

// Observe changes to global variables
b = 0; // globalThis.b = 0;
Observer.observe(globalThis, "b", (mutation) => {
  console.log("New value:", mutation.value);
});

And for updates to local variables, while you can't use the Observer API directly (as local variables aren't associated with a physical object as we have of global variables)...

let b = 0;
Observer.observe(?, 'b', () => { ... });

...you can use a Live function itself to achieve the exact:

(function () {
  "use live";
  console.log('New value:', b);
})();

...and, where necessary, you could next map those changes to an object that you intend to use the Observer API on:

(function () {
  "use live";
  obj.b = b;
})();
Observer.observe(obj, 'b', () => { ... });

Detailed Documentation

Coming soon! The docs in the wiki are for a previous version of UseLive, but may give an idea of advanced concepts.

Examples

Using the UseLive and the Observer API, the following examples work today. While we demonstrate the most basic forms of these scenarios, it takes roughly the same principles to build more intricate equivalents.

Example 1: A Custom Element-Based Counter
└─────────

This is a custom element that works as a counter. Notice that the magic is in its live render() method. Reactivity starts at connected time (on calling the render() method), and stops at disconnected time (on calling dispose)!

customElements.define(
  "click-counter",
  class extends HTMLElement {
    count = 10;

    connectedCallback() {
      // Initial rendering
      this._state = this.render();
      // Static reflection at click time
      this.addEventListener("click", () => {
        this.count++;
      });
    }

    disconnectCallback() {
      // Cleanup
      this._state.abort();
    }

    // Using the LiveFunction constructor
    render = LiveFunction(`
    let countElement = this.querySelector('#count');
    countElement.innerHTML = this.count;
    
    let doubleCount = this.count * 2;
    let doubleCountElement = this.querySelector('#double-count');
    doubleCountElement.innerHTML = doubleCount;
    
    let quadCount = doubleCount * 2;
    let quadCountElement = this.querySelector('#quad-count');
    quadCountElement.innerHTML = quadCount;
  `);
  }
);
<click-counter style="display: block; padding: 1rem;">
  Click me<br />
  <span id="count"></span><br />
  <span id="double-count"></span><br />
  <span id="quad-count"></span>
</click-counter>
Example 2: A Custom URL API
└─────────

This is a simple replication of the URL API - where you have many interdependent properties! Notice that the magic is in its live compute() method which is called from the constructor.

const MyURL = class {
  constructor(href) {
    // The raw url
    this.href = href;
    // Initial computations
    this.compute();
  }

  compute = LiveFunction(`
    // These will be re-computed from this.href always
    let { protocol, hostname, port, pathname, search, hash } = new URL(this.href);

    this.protocol = protocol;
    this.hostname = hostname;
    this.port = port;
    this.pathname = pathname;
    this.search = search;
    this.hash = hash;

    // These individual property assignments each depend on the previous 
    this.host = this.hostname + (this.port ? ':' + this.port : '');
    this.origin = this.protocol + '//' + this.host;
    let href = this.origin + this.pathname + this.search + this.hash;
    if (href !== this.href) { // Prevent unnecessary update
      this.href = href;
    }
  `);
};
// Instantiate
const url = new MyURL("https://www.example.com/path");

// Change a property
url.protocol = "http:"; //Observer.set(url, 'protocol', 'http:');
console.log(url.href); // http://www.example.com/path

// Change another
url.hostname = "foo.dev"; //Observer.set(url, 'hostname', 'foo.dev');
console.log(url.href); // http://foo.dev/path

Getting Involved

All forms of contributions are welcome at this time. Also, implementation details are all up for discussion.

License

MIT.

About

A runtime extension to JavaScript that unlocks Imperative Reactive Programming (IRP) in JavaScript!

Topics

Resources

License

Stars

Watchers

Forks

Sponsor this project

  •  

Contributors 3

  •  
  •  
  •