@webqit/use-live
v0.5.44
Published
Runtime extension to JavaScript that enables Imperative Reactive Programming (IRP) in JavaScript.
Downloads
1,191
Maintainers
Readme
UseLive — Live Mode for JavaScript
Overview • Creating Live Programs • Implementation • Examples • License
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 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 ordinary imperative JavaScript:
"use live";
// Declare values
let count = 5;
let doubleCount = count * 2;
// Log this value live
console.log(doubleCount);// Setup periodic updates
setInterval(() => {
count += 10;
}, 1000);To try:
- Add the following script to your page:
<script src="https://unpkg.com/@webqit/oohtml/dist/main.js"></script>- 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(() => {
count += 10;
}, 1000);
</script>- Watch your console.
To go one step further, update your step 2 to split the logic into two separate scripts:
<script>
"use live";
// Declare values
let count = 5;
let doubleCount = count * 2;
// Setup periodic updates
setInterval(() => {
count += 10;
}, 1000);
</script><script>
"use live";
// Log this value live
console.log(doubleCount);
</script>Watch your console. Reactivity still works.
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
<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,
LiveProgramHandle,
Observer,
} = window.webqit;UseLive in Node.js
// npm install
npm i @webqit/use-live// Import API
import {
LiveFunction,
AsyncLiveFunction,
LiveScript,
AsyncLiveScript,
LiveModule,
LiveProgramHandle,
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.
<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, LiveProgramHandle, Observer } =
window.webqit;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>
// npm install
npm i @webqit/use-live// Import Lite API
import {
AsyncLiveFunction,
AsyncLiveScript,
LiveModule,
LiveProgramHandle,
Observer,
} from "@webqit/use-live/lite";Creating Live Programs
Live Functions
You declare live functions by declaring 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();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();// 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); // 30And 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
AsyncLiveFunctionAPI.
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
AsyncLiveScriptandLiveModuleAPIs.
Consuming Live Programs
Each call to a Live function or script returns back a LiveProgramHandle object that lets you access values exposed by the program.
For Live functions:
const handle = bar();For Live scripts:
const handle = program.execute();For Live HTML scripts - <script>"use live"</script>, the LiveProgramHandle object is available as a direct property of the script element:
console.log(script.liveProgramHandle);Return Value
For functions, the LiveProgramHandle object exposes a value property that carries the program's actual return value:
function sum(a, b) {
"use live";
return a + b;
}const handle = sum(5, 4);
console.log(handle.value); // 9But being a "live" program, handle.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 handle = counter();
console.log(handle.value); // 0The general-purpose, object-observability API: Observer API may be used to observe changes to the value property:
Observer.observe(handle, "value", (mutation) => {
//console.log(handle.value); Or:
console.log(mutation.value); // 1, 2, 3, 4, etc.
});Module Exports
For module programs, the LiveProgramHandle object exposes an exports property that produces the module's exports:
const handle = await program.execute();
console.log(handle.exports); // { count }But being a "live" program, each property in the handle.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 LiveProgramHandle object they return exposes an abort() method that lets us do that:
handle.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.
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>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/pathGetting Involved
All forms of contributions are welcome at this time. Also, implementation details are all up for discussion.
License
MIT.
