Skip to content

Latest commit

 

History

History

wire

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

$mol_wire

A reactivity system. It gives ability to:

  • Make any state observable using only 1.5KB lib. $hyoo_crowd as example.
  • Automatic dynamic track runtime value dependencies and optimize task execution order.
  • Memoize calculations with automatic revalidation. Yes, it completely solves the first of hard problem in computer science.
  • Convert sync API to async and vice versa. Yes, it's a black magic of idempotence.
  • Manage resources automatically with predictable deconstruction moment. Yes, we don't rely on garbage collector.
  • Dramatically reduce source code size and increase reliability by implementing reactive architecture.

Articles about

High level API

Decorators

Proxies

Functions

Structures

Low level API

Debug

Pub/Sub

Reactivity

NPM Bundles

Lib with all production ready $mol_wire modules.

npm install mol_wire_lib

7KB

TypeScript example:

import {
	$mol_wire_solo as solo,
	$mol_wire_plex as plex,
	$mol_wire_method as task,
} from 'mol_wire_lib'

class User {
	@solo age( next = 0 ) { return next }
	@plex finger_exists( id: string, next = true ) { return next }
	@task finger_cut( id: string ) { this.finger_exists( id, false ) }
}

JavaScript example:

const $ = require( 'mol_wire_lib' )

class User {
	age( next = 0 ) { return next }
	finger_exists( id: string, next = true ) { return next }
	finger_cut( id: string ) { this.finger_exists( id, false ) }
}

$.$mol_wire_solo( User.prototype, 'age' )
$.$mol_wire_plex( User.prototype, 'finger_exists' )
$.$mol_wire_task( User.prototype, 'finger_cut' )

Tiny lib to making any state observabe for other $mol_wire based libs.

npm install mol_wire_pub

1.5KB

import { $mol_wire_pub as Publisher } from 'mol_wire_pub'

let counter = 0
const pub = new Publisher

export function state() {
	pub.promote()
	return counter
}

export function increase() {
	++ counter
	pub.emit()
}

export function decrease() {
	-- counter
	pub.emit()
}

Lib to make real DOM reactive.

npm install mol_wire_domm

7.5KB

import { $mol_wire_dom as reactivate } from 'mol_wire_dom'

reactivate( document.body )

Close alternatives

Core concepts

In $mol_wire we build reactive systems using classes that have reactive properties. We represent a reactive property using a class method with an appropriate decorator.

Channels

We define properties in accordance to the pattern of channels. A channel behaves like a getter when called without arguments and as a getter-setter otherwise. This approach proves to be more flexible than others.

Here's an example of a simple channel:

let _title = ''
const title = ( text = _title )=> _title = text

title()                  // getter, returns ''
title( 'Buy some milk' ) // getter-setter, sets and returns 'Buy some milk'
title()                  // getter, returns 'Buy some milk'

You can notice that this is similar to some JQuery methods.

Instead of a plain function and a variable $mol_wire uses class methods with an appropriate decorator:

import { $mol_wire_solo as solo } from 'mol_wire_lib'

class Store {
	@solo title( text = '' ) { return text }
}

const store = new Store()

store.title()                  // getter, returns ''
store.title( 'Buy some milk' ) // getter-setter, sets and returns 'Buy some milk'
store.title()                  // getter, returns 'Buy some milk'

The decorator memoizes what's been returned from the method so when someone uses it as a getter the method itself is not called and instead the memoized value is returned.

Properties either:

  • store values
  • or compute new values based on other properties.

The computed properties will only recompute when one of their dependencies change.

import { $mol_wire_solo as solo } from 'mol_wire_lib'

class User {
	// stores a value
	@solo name_first( next = 'Thomas' ) {
		return next
	}

	// stores a value
	@solo name_last( next = 'Anderson' ) {
		return next
	}

	// computes a value based on other channels
	@solo name() {
		console.log('compute name')
		return `${this.name_first()} ${this.name_last()}`
	}
}

const user = new User()

console.log(user.name()) // logs: 'compute name', 'Thomas Anderson'
console.log(user.name()) // logs: 'Thomas Anderson'

// setter, marks 'name' as a subject to recomputation,
// but doesn't recompute immediately
user.name_last('William')

console.log(user.name()) // logs: 'compute name', 'Thomas William'
console.log(user.name()) // logs: 'Thomas William'

Memoization

Channels marked with a @solo or @plex decorator are memoized until a new value is set to them or one of their dependencies change.

We can use memoization to guarantee that a reference to an object will stay the same:

import { $mol_wire_solo as solo } from 'mol_wire_lib'

class Task {
	@solo timestamp( value = 0 ) {
		return value
	}

	// won't create new instances until `timestamp` is changed
	@solo moment() {
		return new Moment(this.timestamp())
	}
}

Or with memoization we can cache expensive computations:

import { $mol_wire_solo as solo } from 'mol_wire_lib'

class Task {
	@solo title( title = '' ) {
		return title
	}

	// won't recompute
	@solo segmenter() {
		return new Intl.Segmenter()
	}

	// won't recompute until `title` is changed
	@solo mirrored_title() {
		const segmenter = this.segmenter()
		const segments = [ ... segmenter.segment( this.title() ) ]
		return segments.map( s => s.segment ).reverse().join('')
	}
}

With $mol_wire_plex (as in multiplexing) we can store multiple values:

import { $mol_wire_plex as plex } from 'mol_wire_lib'

class Project {
	@plex task( id: number ) {
		return new Task( id )
	}
}

const project = new Project()
const task1 = project.task(1)
const task2 = project.task(2)

Hacking

Another piece of flexibility that channels give is what's called hacking. It allows us to impose custom rules on how channels are read or set:

import {
	$mol_wire_solo as solo,
	$mol_wire_plex as plex,
} from 'mol_wire_lib'

class Task {
	// task has a duration
	@solo duration( dur?: number ) {
		return dur
	}
}

class Project_limited {
	// project has many tasks
	@plex task( id: number ) {
		const task = new Task()

		// the "hacking" technique:
		// when someone tries to get or set the duration for a task
		// we proxy this through our own method imposing a limitation for maximum duration
		task.duration = ( duration ) => this.task_duration( id, duration )

		return task
	}

	@plex task_duration( id: number, duration = 1 ) {
		// clamp the duration to the maximum value
		return Math.min( duration, this.duration_max() )
	}

	duration_max() {
		return 10
	}
}

const project_limited = new Project_limited()

const task_limited = project_limited.task(1)
task_limited.duration(20) // try to set 20 for the duration

console.log(task_limited.duration()) // logs: 10

Destructors

We can take the use of memoization even further by leveraging destructors.

$mol_wire will call a special method named destructor on an object that is no longer needed:

import { $mol_wire_solo as solo } from 'mol_wire_lib'

class ExampleAPI {
	socket: WebSocket

	constructor (
		public api_key : string ,
	) {
		this.socket = new WebSocket(`wss://example.com?api_key=${api_key}`)
	}

	// the special method
	destructor() {
		this.socket.close()
	}
}

class App {
	@solo api_key( value = '' ) {
		return value
	}

	@solo api() {
		return new ExampleAPI( this.api_key() )
	}
}

const app = new App()

// set an api key
app.api_key('testkey1')

// create an instance of WebSocket, establish a connection
app.api()

// change the api key
// this will trigger creation of a new web socket
// and the old one will get destructured
app.api_key('testkey2')

Asynchronous computed values

Unlike many other reactivity systems, $mol_wire allows you to have computeds that depend on asynchronous values.

$mol_wire makes it possible by using an implementaion of Suspense API where an asynchronous task throws an exception to pause the computation until the task resolves. You can read more about it here.

import {
	$mol_wire_solo as solo,
	$mol_wire_sync,
	$mol_wire_async,
} from 'mol_wire_lib'

// produce a value with 1 second delay
function value_async(): Promise<number> {
	return new Promise((resolve) => {
		const value = 25
		setTimeout(() => resolve(value), 1000)
	})
}

class App {
	@solo value() {
		// convert asynchronous function to a synchronous one
		// NOTE closures won't work here,
		// NOTE only use references to pre-defined functions or object methods
		const value_sync = $mol_wire_sync(value_async)

		// treat the value as it is already there
		return value_sync() * 2
	}

	run() {
		console.log(this.value())
	}
}

const app = new App()

// run the application in a Suspense environment
$mol_wire_async(app).run()

// logs: 50

Side effects in asynchronous computations

In $mol_wire we treat values that are computed asynchronously as they're already there. This is thanks to Suspense API: when an asynchronous task starts in a method it throws an exception and when it finishes the method is called again. A more detailed description is here.

Because of that we have to be a little careful about how we make side effects inside our methods.

The @$mol_wire_method decorator (which is usually aliased to @action) prevents methods from being called multiple times:

import {
	$mol_wire_method as action,
	$mol_wire_sync,
} from 'mol_wire_lib'

class App {
	// Auto wrap method to task
	@action main() {
		// Convert async api to sync
		const syncFetch = $mol_wire_sync( fetch )

		this.log( 'Request' ) // 3 calls, 1 log
		const response = syncFetch( 'https://example.org' ) // Sync but non-blocking
		const syncResponse = $mol_wire_sync( response )

		this.log( 'Parse' ) // 2 calls, 1 log
		const response = syncResponse.json() // Sync but non-blocking

		this.log( 'Done' ) // 1 call, 1 log
	}

	// Auto wrap method to sub-task
	@action log( ... args: any[] ) {
		console.log( ... args )
		// No restarts within a portion of a task
	}
}

Cancelling asynchronous tasks

We can cancel asynchronous tasks when we no longer need them by using destructors again.

Here's an example of a cancelable fetch:

import { $mol_wire_sync } from 'mol_wire_lib'

const fetchJSON = $mol_wire_sync( function fetch_abortable(
	input: RequestInfo,
	init: RequestInit = {}
) {
	const controller = new AbortController
	init.signal ||= controller.signal

	const promise = fetch( input, init )
		.then( response => response.json() )

	// assign a destructor to cancel the request
	const destructor = ()=> controller.abort()
	return Object.assign( promise, { destructor } )
} )

Then, we can use it in our computeds:

import { $mol_wire_plex as plex } from 'mol_wire_lib'

class GitHub {
	@plex static issue( id: number ) {
		return fetchJSON( `https://api.github.com/repos/nin-jin/HabHub/issues/${id}` )
	}
}

or a more interesting use case could look like this:

import { $mol_wire_async } from 'mol_wire_lib'

button.onclick = $mol_wire_async( function() {
	const { profile } = fetchJSON( 'https://example.org/input' )
	console.log( profile )
} )

In the above example clicking the button will trigger an HTTP request. If it doesn't resolve until the user clicks again, a new request will be sent and the previous one will get cancelled.

And this is how easy it is to add debounce to this function:

button.onclick = $mol_wire_async( function() {
+	// schedule a timer
+	// the execution won't go past this point until it resolves
+	// if onclick is called again the timer gets rescheduled
+	$mol_wait_timeout( 1000 )
	const { profile } = fetchJSON( 'https://example.org/input' )
	console.log( profile )
} )