Security News
RubyGems.org Adds New Maintainer Role
RubyGems.org has added a new "maintainer" role that allows for publishing new versions of gems. This new permission type is aimed at improving security for gem owners and the service overall.
@webqit/subscript
Advanced tools
This project proposes a new function primitive that lets us open a reactive programming context within JavaScript.
Reactive programming has become one of the most exciting programming paradigms of modern frontend development! While there continues to be varying opinions (and a high degree of polarization) as to what it is and what implementation makes the most sense, you'd realize that everyone is converging on one idea: an automated approach to keeping something (b) in sync with something else (a), such that the expression b = a
is held as a contract throughout the lifetime of the program.
Problem is: in the real world, the concept of “contract” isn't in the design of literal assignment expressions as we have in theory above (nor does it exist in any other imperative operation). One must have to model the equivalent of an imperative expression in functional programming - to have a chance to ensure that the “contract” is kept!
Consider how the following theoretical reactive code would be constructed across a selection of frameworks (ignoring completeness and perfectionism):
let a, b; a = 10, b = a * 2;
// React:
let [ valueA, setValueA ] = useState(10);
let [ valueB, setValueB ] = useState();
useEffect(() => setValueB(valueA() * 2));
// Solid JS:
let [ valueA, setValueA ] = createSignal(10);
let [ valueB, setValueB ] = createSignal();
createEffect(() => setValueB(valueA() * 2));
// Vue:
let a = ref(10);
let b = ref();
watchEffect(() => b.value = a.value * 2);
// Svelte (with equivalent constructs hidden behind a compiler):
let a = 10;
$: b = a * 2;
// etc
Where does it hurt? Why, everywhere!
Having reactivity as a native language feature - this time, reactivity in the literal, imperative form of the language!
You'd realize that as the language engine, we aren't subject to the same userland constraints that hang reactivity at an abstraction. We operate at the root and can conveniently solve for the lowest common denominator.
So, we want to be able to just say (let a, b; a = 10, b = a * 2
) and have it binding - but specifically in a reactive programming context!
Subscript Function is a proposed function primitive that provides this reactive programming context within JavaScript. It “keeps the contract” for the individual expressions and statements that go into its context!
These functions go with a notation as in below...
function** fn() {}
// much like the syntax for generator functions - function fn*() {}
...and the function body is any regular piece of code that needs to stay up to date with its dependencies in fine-grained details.
let var1 = 10;
function** fn() {
console.log(var1);
}
It is, in every way, like a regular function, and can be called any number of times.
fn();
// prints: 10
But it also exposes a .thread()
method that specifically lets us keep it in sync with one or more of its outer dependencies - whenever those change.
var1 = 20
fn.thread( [ 'var1' ] );
// prints: 20
This method passes a list of outer references for which a selection of dependent expressions or statements within the program are rerun - in the order they appear.
let var1 = 10, var2 = 0;
function** fn() {
let localVar1 = var1 * 2;
let localVar2 = var2 * 2;
console.log(localVar1);
console.log(localVar2);
}
var2 = 11;
fn.thread( [ 'var2' ] );
// prints: 22
So, calling the .thread()
method in the example above moves the function's control directly to its second statement, and next to its fourth statement - as this has localVar2
as a dependency. Local state is changed until next time. Statements 1 and 3 are left in the same state as from the last time they were touched.
Now, in logical terms, a .thread()
update follows the implicit dependency graph of the expressions and statements in the function body. This means nothing is ever overrun or underrun throughout the lifetime of the program! (And you're right! Now, it gets harder for applications to not be performant!)
A reactive programming context must be explicitly designated. So we propose using a double star (**
) on the function syntax Discussion Point 1. (And this would be just one star extra on the standard syntax for Generator Functions function* gen() {}
.)
// As function declaration
function** fn( a, b ) {
return a + b;
}
// As function expression
let fn = function**( a, b ) {
return a + b;
}
// As function constructor
// A one-on-one equivalent of the standard function constructor (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/Function)
let fn = new SubscriptFunction( `a`, `b`, `return a + b;` );
Now, being function-based lets us have this everywhere:
// As property
let myObject = {
fn: function**( a, b ) {
return a + b;
}
}
// As method
let myObject = {
**fn( a, b ) {
return a + b;
}
}
// As class method
class MyClass {
**fn( a, b ) {
return a + b;
}
}
subscrFunction.thread()
MethodThe .thread()
method is the reactivity API in Subscript Functions. It constitutes one clear interaction point and enables a one-liner approach to fine-grained reactivity.
It passes a list of the outside variables or properties that have changed; each as an array path.
let returnValue = subscrFunction.thread( path1, ... pathN );
Parameters
path1, ... pathN
- An array path representing each variable, or object property, that has changed.Return Value
The return value of this method depends on the return value of the dependency thread it initiates within the function body.
// Outer dependencies
var a = 10;
var b = 2;
// A function with two possible return values
let sum = function**() {
if ( a > 10 ) {
return Promise.resolve( a + b );
}
return a + b;
}
// Run normally
console.log( sum() );
< 12
// Run a thread with a different return value
a = 20;
console.log( sum.thread( [ 'a' ] ) );
< Promise { 22 }
Expressions and statements in Subscript Function contexts maintain a binding to their dependencies.
For example, variable declarations, with let and var, and assignment expressions, are bound to any references on their right-hand side. (const declarations are an exception as they're designed to be immutable.)
var tense = score > 50 ? 'passed' : 'failed';
So, above, the assignment expression is bound to an external reference score
; and thus responds to an update event for score
.
Where an expression or statement depends on a previous one, a dependency thread is formed, and updates are executed along this thread.
Thus, any subsequent statement, like the one below, having the tense
variable itself as a dependency is updated next...
let message = `Hi ${ candidate.firstName }, you ${ tense } this test!`;
And the update continues to include any subsequent dependents of the message
variable itself... and on to dependents of those dependents... until the end of the dependency thread.
// This. (Having the “message” variable as a dependency.)
let fullMessage = [ message, ' ', 'Thank you!' ].join( '' );
// This. (Having the “fullMessage” variable as a dependency, in addition to the “candidate.username” property.)
let broadcast = { [ candidate.username ]: fullMessage };
// These two. (Having the “broadcast” variable as a dependency.)
let broadcastInstance = new BroadcastMessage( broadcast );
console.log( broadcast );
Subscript Functions employ a mix of compile-time and runtime heuristics to deliver fine-grained reactivity. This lets us enjoy the full range of language features without “loosing” reactivity or trading performance.
For example, expressions that reference deep object properties are bound to updates that actually happen along those paths.
let username = candidate.username;
let avatarUrl = candidate.profile.avatar;
So, above, the first expression responds only when the candidate.username
property is updated or deleted, or when the root object candidate
is changed. And the second expression responds only when the candidate.profile.avatar
property or the parent path candidate.profile
is updated or deleted, or when the root object candidate
is changed.
The above holds even with a dynamic syntax.
let username = candidate[1 ? 'username' : 'name'];.
let avatarUrl = (1 ? candidate : {}).profile?.avatar;
Also, the two expressions continue to be treated individually - as two distinct contracts - even when combined in one declaration.
let username = candidate.username, avatarUrl = candidate.profile.avatar;
Heuristics make it all work with all of ES6+ syntax niceties. Thus, they continue to be two distinct contracts (and reactivity remains fine-grained) even with a destructuring syntax.
let { username, profile: { avatar: avatarUrl } } = candidate;
And even when so dynamically destructured.
let profileProp = ''avatar';
let { username, profile: { [ profileProp ]: avatarUrl } } = candidate;
As another special-syntax case, spread expressions are bound to both the spread element itself and its sub elements.
let candidateCopy = { …candidate };
This means that the expression will re-evaluate when the spread element candidate
changes, and when any of its direct properties change.
Powerful heuristics make it possible to pick up side effects - indirect mutations - made by inner functions within the Subscript Function context itself.
function** fn() {
function sum( a, b ) {
callCount ++;
return a + b;
}
let callCount = 0;
let result = sum( score, 100 );
console.log( 'Number of times we\'ve summed:', callCount );
}
fn();
Above, a side effect happens whenever sum()
is called. Although the console.log()
expression isn't directly dependent on the result = sum()
expression, it is directly dependent on the side effect - callCount
- of sum()
. So, with an update event for score
, the result = sum()
expression runs, and the console.log()
expression runs next.
If these two expressions were to appear in reverse order, as in below…
console.log( 'Number of times we\'ve summed:', callCount );
let result = sum( score, 100 );
…an update event for score
would run only the result = sum()
expression, because, following the runtime's stack-based execution model, the side effect on callCount
would have happened after console.log()
got a chance to run.
When the "test" expression of an "if/else" statement, "switch" statement, or other logical expression contains references, the statement or logical expression is bound to those references. This lets us have conditionals and logic as a contract.
An "if/else" statement is bound to references in its "test" expression.
if ( testExpr ) {
// consequentBlock
} else {
// alternateBlock
}
Above, the "if/else" construct is bound to any references in testExpr
. An update event for any of these gets the construct evaluated again to keep the contract. So, the "test" expression (testExpr
) is run, then, the body of the appropriate branch of the construct is executed as a block.
An “else/if” block is taken as a standalone contract nested within the “else” block of a parent “if/else” contract. In other words, the two forms below are functionally equivalent.
if ( testExpr1 ) {
// consequentBlock1
} else if ( testExpr2 ) {
// consequentBlock2
} else {
// alternateBlock
}
if ( testExpr1 ) {
// consequentBlock1
} else {
if ( testExpr2 ) {
// consequentBlock2
} else {
// alternateBlock
}
}
A "switch" statement is bound to references in its "switch/case" conditions.
switch( operandExpr ) {
case caseExpr1:
// consequentBlock1
break;
case caseExpr2:
// consequentBlock2
break;
default:
// defaultBlock
}
Above, the "switch" construct is bound to any references in operandExpr
, caseExpr1
and caseExpr2
. An update event for any of these gets the construct evaluated again to keep the contract. So, the "switch/case" conditions (operandExpr === caseExpr1
| operandExpr === caseExpr2
| operandExpr === null
) are run, then, the body of the appropriate branch of the construct is executed as a block.
Expressions with logical and ternary operators also work as conditional contracts. These expressions are bound to references in their “test” expression.
// Logical expression
let result = testExpr && consequentExpr || alternateExpr;
// Ternary expression
let result = testExpr ? consequentExpr : alternateExpr;
In all conditional constructs above, the contract is that updates to the “test” expressions themselves result in the rerun of the appropriate branch of the construct. The selected branch is rerun as a block, not in fine-grained execution mode.
if ( testExpr ) {
addBadge( candidate );
console.log('You\'ve got a badge');
} else {
removeAllBadges( candidate );
console.log('You\'ve lost all badges');
}
So, above, an update to testExpr
runs the selected branch as a block - involving its two statements.
But being each a contract of their own, individual expressions and statements inside a conditional context also respond to update events in isolation. This time, the conditions in context have to be “true” for the expression or statement to rerun.
So, above, the addBadge()
and removeAllBadges()
expressions are both bound to the reference candidate
. But on an update to candidate
, only one of these expressions is run depending on the state of the condition in context - testExpr
.
In a nested conditional context…
if ( parentTestExpr ) {
if ( testExpr ) {
}
}
…all conditions in context (parentTestExpr
>> testExpr
) have to be “true” for an update to take place.
In all cases, the "state" of all conditions in context are determined via memoization, and no re-evaluation ever takes place on the “test” expressions.
“Switch” statements and logical and ternary expressions have this fine-grained behaviour in their own way.
When the parameters of a loop ("for" loop, "while" and "do … while" loop) contain references, the loop is bound to those references. This lets us have loops as a contract.
A "for" loop is bound to references in its 3-statement definition.
for (initStatement; testStatement; incrementStatement) {
// Loop block
}
So, in the loop above, an update event for any references in initStatement
; testStatement
; incrementStatement
reruns the loop to keep the contract.
As with a "for" loop, a "while" and "do ... while" loop are bound to references in their "test" expression.
while (testExpr) {
// Loop block
}
do {
// Loop block
} while (testExpr);
So, in each case above, an update event for any references in testExpr
reruns the loop to keep the contract.
These loops are bound to references in their iteratee.
for (let value of iteratee) {
// Loop body
}
for (let key in iteratee) {
// Loop body
}
So, in each case above, an update event for any references in iteratee
reruns the loop to keep the contract.
In all loop constructs above, the contract is that updates to the iteration parameters themselves result in the restart of the loop. The loop body, in each iteration, is run as a block, not in fine-grained execution mode.
var start = 0;
var items = [ 'one', 'two', 'three', 'four', 'five' ];
var targetItems = [];
var prefix = '';
function** fn() {
for ( let index = start; index < items.length; index ++ ) {
console.log( `Current iteration index is: ${ index }, and value is: '${ items[ index ] }'` );
targetItems[ index ] = prefix + items[ index ];
}
}
fn();
So, above, an update to any of start
, items
, and items.length
gets the loop restarted…
start = 2;
fn.thread( [ 'start' ] );
items.unshift( 'zero' );
fn.thread( [ 'items', 'length' ] );
…with each iteration running the loop body as a block - involving its two statements.
But being each a contract of their own, individual expressions and statements in the body of a loop also respond to update events in isolation. This time, an update happens in-place in each of the previously-made iterations of the loop.
So, above, on updating the reference prefix
, the second statement (specifically) in each existing round of the loop responds to keep their contract. Thus, each entry in targetItems
gets updated with prefixed values.
prefix = 'updated-';
fn.thread( [ 'prefix' ] );
A “for … of, for … in” loop further has the unique characteristic where each round of the loop maintains a direct relationship with its corresponding key in the iteratee. Now, on updating the value of a key in iteratee
in-place, the associate round (specifically) also runs in-place to keep its contract.
var items = [ { name: 'one' }, { name: 'two' }, { name: 'three' }, { name: 'four' }, { name: 'five' } ];
function** fn() {
for ( let entry of items ) {
let index = items.indexOf( entry );
console.log( `Current iteration index is: ${ index }, and name is: '${ entry.name }'.` );
targetItems[ index ] = items[ index ].name;
}
}
fn();
entries[ 2 ] = { name: 'new three' };
fn.thread( [ 'items', 2 ] );
Now, the console reports…
Current iteration index is: 2, and name is: 'new three'.
…and index 2 of targetEntries
is updated.
If we mutate the name
property of the above entry in-place, then it gets even more fine-grained: only the dependent console.log()
expression in that round runs to keep its contact.
entries[ 2 ].name = 'new three';
fn.thread( [ 'items', 2, 'name' ] );
Now, the console reports…
Current iteration index is: 2, and name is: 'new three'.
This granular reactivity makes it often pointless to trigger a full rerun of a loop, offering multiple opportunities to deliver unmatched performance.
Break
And Continue
StatementsFine-grained updates observe break
and continue
statements, even when these redirect control to a parent block using labels.
let entries = { one: { name: 'one' }, two: { name: 'two' } };
function** fn() {
parentLoop: for ( let propertyName in entries ) {
childLoop: for ( let subPropertyName in entries[ propertyName ] ) {
If ( propertyName === 'two' ) {
break parentLoop;
}
console.log( propertyName, subPropertyName );
}
}
}
fn();
So, above, on updating the entries
object, the nested loops run as expected, and the child loop effectively breaks the parent loop at the appropriate point.
fn.thread( [ 'entries' ] );
If we mutated the object in-place to make just the child loop rerun…
fn.thread( [ 'entries', 'two' ] );
…the break
directive in the child loop would be pointing to a parent loop that isn't running, but this would be harmless. The child loop would simply exit as it would if the parent were to actually break at this point.
But if we did the above with a continue
directive, the child loop would also exit as it would if the parent were to actually receive control back at this point, without control actually moving to a non-running parent.
A Custom Element Example
This custom element has it's render()
method as a Subscript Function.
// Outer dependency
let count = 10;
customElements.define( 'click-counter', class extends HTMLElement {
connectedCallback() {
// Full rendering at connected time
// The querySelector() calls below are run
this.render();
// Fine-grained rendering at click time
// The querySelector() calls below don't run again
this.addEventListener( 'click', () => {
count ++;
this.render.thread( [ 'count' ] );
} );
}
**render() {
let countElement = document.querySelector( '#count' );
countElement.innerHTML = count;
let doubleCount = count * 2;
let doubleCountElement = document.querySelector( '#double-count' );
doubleCountElement.innerHTML = doubleCount;
let quadCount = doubleCount * 2;
let quadCountElement = document.querySelector( '#quad-count' );
quadCountElement.innerHTML = quadCount;
}
} );
Above, render()
is called only once. Subsequent updates employ its .thread()
method to update just the relevant contracts in the block. Fine-grained reactivity and optimal performance is gained.
**
) work?This Polyfill is a work in progress. But it is usable today.
> Install via npm
npm i @webqit/subscript
import { SubscriptFunction, SubscriptClass } from '@webqit/subscript';
> Include from a CDN
<script src="https://unpkg.com/@webqit/subscript/dist/main.js"></script>
const { SubscriptFunction, SubscriptClass } = WebQit.Subscript;
The current polyfill only supports the constructable form of Subscript Function.
let fn = new SubscriptFunction( `a`, `b`, `return a + b;` );
But the double star syntax is supported from within the function itself.
let fn = new SubscriptFunction( `
function** sum( a, b ) {
return a + b;
}
let result = sum( score, 100 );
// result = sum.thread();
` );
Subscript Functions as class methods are currently only supported using a SubscriptClass()
mixin.
class MyClass extends SubscriptClass() {
static get subscriptMethods() {
return [ 'sum' ];
}
sum( a, b ) {
return a + b;
}
}
class MyClass extends SubscriptClass( HTMLElement ) {
static get subscriptMethods() {
return [ 'render' ];
}
render() {
}
}
Watch the issues tab for new known issues
To visualize dependency threads in a live .thread()
update, we've provided a custom element named subscript-player
.
Simply include a pair of scripts in your page...
<script crossorigin defer src="https://unpkg.com/@webqit/subscript/dist/console-element.js"></script>
<script crossorigin defer src="https://unpkg.com/@webqit/subscript/dist/player-element.js"></script>
Wrap any piece of code with it... (or edit right in the UI.)
<subscript-player automode="play">
let count = 10, doubleCount = count * 2, quadCount = doubleCount * 2;
console.log( count, doubleCount, quadCount );
</subscript-player>
Then click on local varaibles to see their dependency threads.
To inspect Subscript Methods and their dependency threads in a live custom element that you've designed, we've provided a custom element named subscript-inspector
.
Simply include a pair of scripts in your page...
<script crossorigin defer src="https://unpkg.com/@webqit/subscript/dist/console-element.js"></script>
<script crossorigin defer src="https://unpkg.com/@webqit/subscript/dist/inspector-element.js"></script>
Wrap your custom element markup with it...
<subscript-inspector>
<my-counter></my-counter>
</subscript-inspector>
Then inspect each Subscript Method while you interact with your element. (See this REPL for an example.)
We'd be super excited to have you raise an issue, make a PR, or join in the discussion at Subscript's Github Discussions.
And, wouldn't you give a star for this 🤨 ?
To report bugs or request features, please submit an issue.
MIT.
FAQs
Customizable JavaScript runtime
The npm package @webqit/subscript receives a total of 31 weekly downloads. As such, @webqit/subscript popularity was classified as not popular.
We found that @webqit/subscript demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
RubyGems.org has added a new "maintainer" role that allows for publishing new versions of gems. This new permission type is aimed at improving security for gem owners and the service overall.
Security News
Node.js will be enforcing stricter semver-major PR policies a month before major releases to enhance stability and ensure reliable release candidates.
Security News
Research
Socket's threat research team has detected five malicious npm packages targeting Roblox developers, deploying malware to steal credentials and personal data.