Skip to content

Instantly share code, notes, and snippets.

@nin-jin
Last active October 6, 2024 06:38
Show Gist options
  • Save nin-jin/6b9765fb9d0d50c2e1d37689008f5357 to your computer and use it in GitHub Desktop.
Save nin-jin/6b9765fb9d0d50c2e1d37689008f5357 to your computer and use it in GitHub Desktop.
JS Proposal: Auto Wire

Reactivity API to wire any reactive libs and native states together. It allows to mix different reactive libs, use async functions with automatic dependency tracking, observe any states in same way, make native DOM reactive etc.

Rationale

Current State

  1. Most of modern apps based on reactive paradigm.
  2. Popular and promising approach is pull-reactivity.
  3. Pull-reactivity achieves automatic dependency trackig, automatic data-flow optimization and resources lifetime control. See the analysis of reactivity approaches.
  4. Famed pull-reactivity libs: MobX, VueJS, $mol.
  5. All implementations have to use global variable to connect observers with observables.

Fundamental Issues

  1. Every implementations has different global variables with different API. Modules based on different libs can't work together directly.
  2. Observing is required for reactivity, but native API's isn't reactive. Some native values can be observed through different API (onhashchange for location, MutationObserver for DOM etc), but some can't (sessionStorage, window.title etc)
  3. Tracking through global variable works incorrectly with async functions. MobX have to use generators instead.

Proposal

Design Concept

  1. All native values can be observed through the same API.
  2. Any statefull lib can be easely adopted for observing with minimal impact on it's size.
  3. Fast enablinng/disabling autowire.
  4. Minimal impact on performance/memory when autowire is disabled.
  5. Async functions support. await and yield stores current subscriber before call and restores after.
  6. DOM reactivity support. When browsers uses element to render docuemnt it uses autowire to subscribeto any publishers including other DOM elements.
  7. Cyclic subscriptions throws Wire.CyclicError error on subscribe.

API

declare namespace Wire {

	/**
	 * Current subscriber that auto wire with all touching publishers.
	 */
	export let auto: PubSub | null
	
	/**
	 * Reactive publisher with minimal memory cost.
	 */
	export class Pub {

		/**
		 * List of all subscribed subscribers.
		 * Can be observed through subscriber too.
		 */
		subs: PubSub[]

		/**
		 * Notify all subscribed subscribers about changes related to this publisher.
		 */
		emit(): void

	}

	/**
	 * Reactive subscriber + publisher (republisher).
	 */
	export class PubSub extends Pub {

		/**
		 * List of all connected publishers.
		 * Can be observed through Sub too.
		 */
		pubs: Pub[]

		/**
		 * Begin auto wire to publishers.
		 */
		begin(): void

		/**
		 * Promotes next publisher to auto wire its together.
		 * Can be executed only between `begin` and `end`.
		 */
		promo( pub: Pub ): void
			
		/**
		 * Returns next already auto wired publisher. It can be easely repormoted.
		 * Can be executed only between `begin` and `end`.
		 */
		next(): Pub | null
			
		/**
		 * Ends auto wire to publishers and unsubscribes from unpromoted publishers.
		 * Executed automatically when cuurent fuction ends.
		 */
		end(): void

		/**
		 * Called when some publisher emits.
		 * Can be overrided with custom logic.
		 * Reemits to self subscribers by default.
		 */
		absorb( pub?: Pub ): void
		
		/**
		 * Unsubscribes from all publishers then emits to subscribers.
		 */
		destructor(): void

	}
	
}

Usage Examples

Reactions

/**
 * Simple class with reactivity support.
 */
class ObservableUser {
	
	#name_pub = new Wire.Pub
	#name = 'Anonymous'
	
	get name() {
		Wire.auto?.promo( this.#name_pub )
		return this.#name
	}

	set name( next: string ) {
		this.#name = next
		this.#name_pub.emit()
	}

}

/**
 * Simple reactive observer.
 */
class ReactiveLogger extends Wire.PubSub {
    
    #pull: ()=> unknown
    
    constructor(
        pull: ()=> unknown
    ) {
        super()
        this.#pull = pull
    }
    
    absorb() {
        requestAnimationFrame( ()=> this.#pull() )
    }
    
    pull() {
        this.begin()
        try {
            console.log( this.#pull() )
        } finally {
            this.end()
        }
        this.emit()
    }
    
}

async function test() {
    
    document.title = 'Hello'
    
    const alice = new ObservableUser
    const logger = new ReactiveLogger( ()=> document.title + alice.name )
    
    // initial pull and auto wire with publishers
    logger.pull()
    
    // change reactive state
    alice.name = 'Alice'
    document.title = 'Bye'
    
    // unsubscribe logger
    logger.destructor()
    
    // Prints "HelloAnonymous", then "HelloAlice", then "ByeAlice" in the next frame
    
}

Reactive DOM

<input id="input" />
<p id="output"></p>
const input = document.getElementById('input')
const output = document.getElementById('output')

Object.defineProperty( output, 'innerText', {
	get: ()=> 'Hello ' + input.value
} )

Polyfills

  • $mol_wire - TS lib with approach like proposed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment