Pausable stackful coroutines (fibers) give ability to write simple but responsive and concurrent code.
It can be used for:
- Automatic pause long task every 16 ms to ensure 60fps.
- Concurrent execution of different tasks (not serial).
- Abort incomplete task (with subtasks) as reaction on event.
- Write simple (synchronous) code.
- Improve performance because synchronous code can be better optimized by JIT.
- Call synchronous API (like Array:reduce) without loosing of concurrency.
// Fiber - lightweight thread, that has separate call stack.
// Multiple fibers concurrently executes in one system thread.
declare class Fiber< Result > extends Promise< Result > {
// Fiber that are executing now.
static current : Fiber< any > | null
// Freezes current fiber until promise will be resolved.
// After, resumes fiber and returns result or rethrows an exception if promise is rejected.
// Throws NotInFiberError if called outside any fiber.
static wait< Result >( promise : PromiseLike< Result > ) : Result
// Executes function in separate fiber
static run( task : ()=> Result ) : Fiber< Result >
// Abort execution.
// Do nothing if is already completed.
abort : ()=> void
}
Function that returns control to main loop every 8 ms:
let deadline = Date.now() + 8
function quant() {
// Do nothing if fibers isn't supported
if( typeof Fiber !== 'function' ) return
// Do nothing if called outside any fiber
if( !Fiber.current ) return
// Do nothing until deadline
if( Date.now() < deadline ) return
// Stop current fiber until next frame
Fiber.wait( new Promise( requestAnimationFrame ) )
// already in next animation frame
dealine = Date.now() + 8
}
Quantized json parsing that don't block event loop more than ~8 ms independent on string size:
json_parse( str ) : any {
return JSON.parse( str , ( key , value )=> ( quant() , value ) )
}
Usage:
import { promisify } from 'util'
import { readFile } from 'fs'
const readFileAsync = promisify( readFile )
// Some synchronous task
function log_config() {
// Load large JSON
const buffer = Fiber.wait( fs.readFileASync( 'config.json' ) )
// Parse json without long thread blocking if can
const json = json_parse( buffer )
// Print json to console
console.log( json )
}
// Spawn fiber
Fiber.run( log_config )
Very huge rendering (over 9000ms):
// Render large tree.
function render_tree( count ) {
// [ 0 , 1 , 2 , ... ]
const numbers = [ ... Array( count ) ].map( ( _ , i ) => i )
// [ <iframe/> , <iframe/> , ... ]
const leafs = render_branch( document.body , numbers )
console.log( leafs )
}
// Render branch.
function render_branch( parent , numbers ) {
// Render element for branch.
const el = document.createElement( 'div' )
parent.appendChild( el )
// Render few leafs and return its.
if( numbers.length <= 5 ) return numbers.map( render_leaf.bind( el ) )
// Split numbers to two buckets.
const center = Math.floor( numbers.length / 2 )
const left = numbers.slice( 0 , center )
const right = numbers.slice( center )
// Render sub branches and join returned leafs
return [ ... render_branch( el , left ) , ... render_branch( el , right ) ]
}
function render_leaf( parent ) {
// Ability to free main thread every 8 ms
quant()
// Do hard work (iframes are very expansive)
const el = document.createElement( 'iframe' )
parent.appendChild( el )
return el
}
Start rendering in separate fiber:
let rendering = Fiber.run( ()=> render_tree( 1000 ) )
Abort old rendering and start new on event:
// On any external event
window.onmessage( count => {
// Stop revious rendering
rendering.abort()
// Start new rendering
rendering = Fiber.run( ()=> render_tree( count ) )
} )
Simple fetch
wrapper:
function get_json( url : string ) : any {
const controller = new AbortController();
const signal = controller.signal;
// Wait for server response
const response = Fiber.wait( fetch( url , { signal } ) )
// Wait for json parse
const json = Fiber.wait( response.json() )
return json
}
Optional asynchrony in other code:
let cache : { beta : boolean }
function get_config() : typeof cache {
// Fill cache if not exists
if( !cache ) cache = get_json( 'example.org' ).flags
return cache
}
Fiber.run( ()=> {
// Will suspend while htt requesting
console.log( get_config() )
// Instant returns data from cache
console.log( get_config() )
} )
// Async in fiber
function main() {
const promise = fetch( '//example.org' )
const response = Fiber.wait( promise )
const json = Fiber.wait( response.json() )
console.log( json )
}
const fiber = Fiber.run( main )
// Fiber in async
async function main2 () {
console.log( 'start' )
await fiber
console.log( 'finish' )
})
main2()
Some features can be transparently disabled if fibers isn't supported.
// Continue in next tick if possible
function tick() {
// Do nothing if fibers isnt' supported
if( typeof Fiber !== 'function' ) return
// Do nothing if called outside any fiber
if( !Fiber.current ) return
// Wait few time
Fiber.wait( new Promise( process.nextTick ) )
}
Usage of global variables and finally
block together should be changed to support execution in fibers
let context = null
function foo( callback1, callback2 ) {
const prev = {}
context = {}
try {
// If `Fiber.wait` is called other `foo` execution can change context
callback1()
// Then we get wrong context there
callback2()
} finally {
context = prev
}
}
const context = new WeakMap
function foo( callback1, callback2 ) {
// Every context is fiber bound
const prev = context.get( Fiber.current )
context.set( Fiber.current , {} )
try {
// Other `foo` execution don't change our context
callback1()
// Then we get right context there
callback2()
} finally {
context.set( Fiber.current , prev )
}
}
- node-fibers - NodeJS native extension that adds fibers to v8 runtime.
- f-promise - Wrapper around
node-fibers
with API like proposed. - $mol_fiber - VanillaJS fibers implementation based on restarts.
- Suspense API in ReactJS based on restarts.
- Cancellation in BlueBird - Cancellation API in promises.
- Meteor - based on
node-fibers
popular web-framework.