├── .example ├── index.js └── stateful-function.js ├── README.md ├── TODO.md ├── context.js ├── effect.js ├── hoist.js ├── iterable.js ├── package.json ├── state.js ├── stateful-functions.js ├── stateful.js └── symbol.js /.example/index.js: -------------------------------------------------------------------------------- 1 | stateful-function.js -------------------------------------------------------------------------------- /.example/stateful-function.js: -------------------------------------------------------------------------------- 1 | import { stateful, useState, useEffect, useContext, provideContext, Context, Top } from ".." 2 | 3 | const ctx= new Context() 4 | 5 | function a( name){ 6 | const [ clicks, setClicks, getClicks] = useState( 0) 7 | function click(){ 8 | setClicks( getClicks()+ 1) 9 | } 10 | function reset(){ 11 | setClicks( 0) 12 | } 13 | const context= useContext( ctx) 14 | if( context){ 15 | console.log(JSON.stringify({ type: "ctx", name, context })) 16 | } 17 | useEffect( _=> { 18 | console.log(JSON.stringify({ type: "eff", name, context, clicks})) 19 | return ()=> console.log(JSON.stringify({ type: "cln", name, context, clicks})) 20 | }) 21 | return { 22 | click, 23 | reset 24 | } 25 | } 26 | 27 | function doA(){ 28 | console.log(JSON.stringify({ type: "doA" })) 29 | const { click, reset }= stateful( a)( "doA") 30 | click() 31 | click() 32 | reset() 33 | click() 34 | click() 35 | } 36 | 37 | function doAAgain(){ 38 | console.log(JSON.stringify({ type: "doAAgain" })) 39 | const { click, reset }= stateful( a)( "doAAgain") 40 | click() 41 | click() 42 | reset() 43 | click() 44 | click() 45 | } 46 | 47 | function doAsInterleaved(){ 48 | console.log(JSON.stringify({ type: "doAsInterleaved" })) 49 | const 50 | { click: click1, reset: reset1}= stateful( a)( "doAsInterleaved1"), 51 | { click: click2, reset: reset2}= stateful( a)( "doAsInterleaved2") 52 | click1() 53 | click2() 54 | reset1() 55 | click1() 56 | click2() 57 | } 58 | 59 | function doContext(){ 60 | console.log(JSON.stringify({ type: "doContext", step: 0})) 61 | const fn= stateful( a) 62 | fn( "no-context-setup") 63 | const runner= stateful( function exec( pass){ 64 | fn( "no-context-running-"+ pass) 65 | provideContext( ctx,{ magic: "content"}) 66 | provideContext( ctx,{ magic: "beans"}) 67 | fn( "has-context-beans-"+ pass) 68 | provideContext( ctx,{ magic: "beanstalk"}) 69 | provideContext( ctx,{ magic: "clouds"}) 70 | fn( "has-context-clouds-"+ pass) 71 | provideContext( ctx,{ magic: "castle"}) 72 | }) 73 | fn("runner-defined") 74 | runner("run1") 75 | fn("run-1") 76 | 77 | console.log() 78 | runner("run2") 79 | fn("run-2") 80 | } 81 | 82 | if( typeof require!== undefined&& require.main=== module){ 83 | doA() 84 | doAAgain() 85 | doAsInterleaved() 86 | doContext() 87 | } 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # stateful-functions 2 | 3 | > Functions with state, effect, & context. 4 | 5 | Let's be honest: I'm trying to copy React Hooks, without any React. 6 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | * hoist properties & symbols with getter/setters 2 | * uniquely identify wrapped with unique id 3 | * record all contexts accessed during lifecycle & signal all listeners that we are not providing 4 | -------------------------------------------------------------------------------- /context.js: -------------------------------------------------------------------------------- 1 | import { Stack, Top} from "./stateful.js" 2 | import { Args} from "./symbol.js" 3 | export class Context{ 4 | constructor(){ 5 | } 6 | } 7 | 8 | // 9 | const _contexts= new WeakMap() 10 | 11 | function walkContextsTo( ctx, stack= Stack()){ 12 | while( stack.length){ 13 | const 14 | ctxs= _contexts.get( stack), 15 | value= ctx&& ctx.get( ctx) 16 | if( value){ 17 | return { 18 | ctx, 19 | stateful: stack[ stack.length- 1], 20 | value 21 | } 22 | } 23 | stack= stack[ stack.length- 2] 24 | } 25 | } 26 | 27 | let currentContext= new WeakMap() 28 | export function useContext( ctx){ 29 | const 30 | top= Top(), 31 | cached= currentContext.get( top), 32 | provider= walkContextsTo( ctx), 33 | value= provider&& provider.value 34 | if( cached!== value){ 35 | currentContext.set( top, value) 36 | if( cached){ 37 | // de-listen to whomever we were listening to & listen to this new 38 | const 39 | listeners= cached.listeners, 40 | i= listeners.indexOf( cached) 41 | if( i!== -1){ 42 | listeners.splice( i, 1) 43 | } 44 | } 45 | if( provider&& provider.value){ 46 | provider.value.listeners.push( top) 47 | } 48 | } 49 | return provider&& provider.value&& provider.value.value 50 | } 51 | 52 | export function provideContext( ctx, value){ 53 | const 54 | stack= Stack(), 55 | hadCtxs= _contexts.get( stack), 56 | newCtxs= !hadCtxs&& new WeakMap(), 57 | ctxs= hadCtxs|| newCtxs, 58 | oldCtx= hadCtxs&& hadCtxs.get( ctx), 59 | changed= oldCtx&& oldCtx.value!== value 60 | if( newCtxs){ 61 | _contexts.set( stack, newCtxs) 62 | } 63 | if( !oldCtx){ 64 | ctxs.set( ctx, { value, listeners: []}) 65 | }else{ 66 | oldCtx.value= value 67 | } 68 | if( changed){ 69 | for( let listener of oldCtx.listeners){ 70 | const args= listener[ Args] 71 | listener( ...args) 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /effect.js: -------------------------------------------------------------------------------- 1 | import { Effect, EffectCleanup} from "./symbol.js" 2 | import { Top} from "./stateful.js" 3 | 4 | export function useEffect( cb){ 5 | const stateful= Top() 6 | stateful[ Effect]= cb 7 | } 8 | 9 | export const UseEffect= useEffect 10 | export default useEffect 11 | -------------------------------------------------------------------------------- /hoist.js: -------------------------------------------------------------------------------- 1 | export function symlinkProps( src, target){ 2 | const symlinks= {} 3 | for( const descriptor of getAllPropertyDescriptors( src)){ 4 | const name= descriptor.name|| descriptor.symbol 5 | if( target[ name]!== undefined){ 6 | continue 7 | } 8 | symlinks[ name]= { 9 | configurable: descriptor.configurable, 10 | enumerable: descriptor.enumerable, 11 | get: function(){ 12 | return src[ name] 13 | }, 14 | set: function( value){ 15 | src[ name]= value 16 | } 17 | } 18 | } 19 | Object.defineProperties( target, symlinks) 20 | return target 21 | } 22 | -------------------------------------------------------------------------------- /iterable.js: -------------------------------------------------------------------------------- 1 | import stateful from "./stateful.js" 2 | 3 | export async function *iterable( fn){ 4 | let wake 5 | const 6 | pending= [], 7 | wrapped= stateful( fn, { ontick: function( val){ 8 | pending.push( val) 9 | notify( pending) 10 | }) 11 | while( true){ 12 | yield pop( pending) 13 | } 14 | } 15 | 16 | async function pop( arr){ 17 | while( true){ 18 | if( arr.length){ 19 | return arr.pop() 20 | } 21 | await wait( arr) 22 | } 23 | } 24 | 25 | function notify( o){ 26 | const listeners= _wait.get( o) 27 | if( !listeners){ 28 | return 29 | } 30 | listeners.forEach( _run) 31 | _wait.set( o, []) 32 | } 33 | async function wait( o){ 34 | const 35 | defer= Defer(), 36 | resolve= defer.resolve.bind( defer) 37 | oldListeners= _wait.get( o) 38 | if( oldListeners){ 39 | oldListeners.push( resolve) 40 | }else{ 41 | _wait.set( o, [ resolve]) 42 | } 43 | return defer.promise 44 | } 45 | const _wait= new WeakMap() 46 | function _run( fn){ 47 | fn() 48 | } 49 | 50 | 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stateful-functions", 3 | "version": "1.0.0", 4 | "description": "Stateful functions that can use state, effects, & context.", 5 | "main": "stateful-functions.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "example": "node -r esm .example" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://archive.eldergods.com/stateful-functions" 13 | }, 14 | "keywords": [ 15 | "react-hooks", 16 | "stateful" 17 | ], 18 | "author": "jauntywunderkind", 19 | "license": "MIT", 20 | "module": "stateful-functions.js", 21 | "esm": true, 22 | "devDependencies": { 23 | "esm": "^3.0.84" 24 | }, 25 | "dependencies": { 26 | "set-immediate-shim": "^2.0.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /state.js: -------------------------------------------------------------------------------- 1 | import { Top} from "./stateful.js" 2 | import { Args} from "./symbol.js" 3 | 4 | const _state= new WeakMap() 5 | 6 | export function useState( val){ 7 | const stateful= Top() 8 | 9 | // retrieve existing 10 | const existing= _state.get( stateful) 11 | if( existing){ 12 | return existing 13 | } 14 | 15 | // state 16 | function getValue(){ 17 | return state[ 0] 18 | } 19 | function setValue( val){ 20 | const effected= val!== _val 21 | state[ 0]= val 22 | if( effected){ 23 | // rerun ourselves 24 | stateful( ...stateful[ Args]) 25 | } 26 | } 27 | 28 | // marshal, save & return 29 | const state= [ _val, setValue, getValue] 30 | _state.set( stateful, state) 31 | return state 32 | } 33 | export const UseState= useState 34 | export default useState 35 | -------------------------------------------------------------------------------- /stateful-functions.js: -------------------------------------------------------------------------------- 1 | export { useEffect, UseEffect} from "./effect.js" 2 | export { useState, UseState} from "./state.js" 3 | export { stack, Stack, stateful, Stateful, top, Top} from "./stateful.js" 4 | export { Context, provideContext, useContext} from "./context.js" 5 | -------------------------------------------------------------------------------- /stateful.js: -------------------------------------------------------------------------------- 1 | import { Args, Effect, EffectCleanup} from "./symbol.js" 2 | 3 | //// state holding //// 4 | // generally for internal use 5 | 6 | let _stack= [] 7 | export function stack(){ 8 | return _stack 9 | } 10 | export const Stack= stack 11 | export function top(){ 12 | const top= _stack[ _stack.length- 1] 13 | return top 14 | } 15 | export const Top= top 16 | 17 | //// interface //// 18 | 19 | /** 20 | * Turn a vanilla function into a stateful function 21 | * @param {function} inputFn - a function to build a stateful wraper for 22 | * @param [thisArg] - optional "this" to call inputFn with. By default, will pass through `this`. if null, will pass nothing ever, but is faster. 23 | * @returns {function} a "wrapped" stateul version of the inputFn with stateful behaviors 24 | */ 25 | export function stateful( inputFn, { ontick, thisArg}= {}){ 26 | let execStack 27 | const 28 | name= inputFn.name+ "Stateful", 29 | call= thisArg=== undefined? ( args)=> { 30 | return inputFn.call( this, ...args) 31 | }: thisArg!== null? function( args){ 32 | return inputFn.call( thisArg, ...args) 33 | }: function( args){ 34 | return inputFn( ...args) 35 | }, 36 | wrapper= {[ name]: function( ...args){ 37 | // add ourselves to stack 38 | const oldStack= _stack 39 | execStack= _stack= _stack.concat( oldStack, wrapped) 40 | 41 | // cleanup any existing run 42 | const cleanup= wrapped[ EffectCleanup] 43 | if( cleanup){ 44 | cleanup() 45 | wrapped[ EffectCleanup]= null 46 | } 47 | 48 | // save args & run 49 | wrapped[ Args]= args 50 | const 51 | val= call( args), 52 | effect= wrapped[ Effect] 53 | // our "render" event just happened 54 | if( ontick){ 55 | ontick( val, wrapped) 56 | } 57 | 58 | // run new effects 59 | if( effect){ 60 | // clear effects 61 | wrapped[ Effect]= null 62 | // run 63 | const cleanup= effect() 64 | if( cleanup){ 65 | // save cleanup 66 | wrapped[ EffectCleanup]= cleanup 67 | } 68 | } 69 | 70 | // restore stack 71 | _stack= oldStack 72 | return val 73 | }}, 74 | wrapped= wrapper[ name] 75 | return wrapped 76 | } 77 | export const Stateful= stateful 78 | export default stateful 79 | 80 | 81 | -------------------------------------------------------------------------------- /symbol.js: -------------------------------------------------------------------------------- 1 | export const 2 | effect= Symbol.for("stateful-function:effect"), 3 | Effect= effect, 4 | effectCleanup= Symbol.for("stateful-function:effect-cleanup"), 5 | EffectCleanup= effectCleanup, 6 | contextValue= Symbol.for("stateful-function:context-value"), 7 | ContextValue= contextValue, 8 | rerender= Symbol.for("stateful-function:rerender"), 9 | Rerender= rerender, 10 | args= Symbol.for("stateful-function:args"), 11 | Args= args 12 | --------------------------------------------------------------------------------