├── es6 ├── future.js ├── observable.js └── promise.js └── README.md /es6/future.js: -------------------------------------------------------------------------------- 1 | const Future = (task) => ({ 2 | // map takes function and returns a new Future 3 | // where the value of the new Future has f applied to it 4 | map: f => Future((reject, resolve) => 5 | task(reject, x => resolve(f(x)))), 6 | 7 | // chain takes a function and returns a Future 8 | // f is expected to run on the value of the future and return a Future 9 | // instead of returning a Future of a Future, chain subscribes to the future returned by `f` 10 | // note that the inner Future is not forked until the outer future is forked - it remains lazy 11 | // the new Future (the one we care about) is resolved, when the inner Future is resolved 12 | chain: f => Future((reject, resolve) => 13 | task(reject, x => 14 | f(x).fork(reject, resolve))), 15 | 16 | // really fork == task; I'm just making the API explicit, here 17 | fork: (reject, resolve) => 18 | task(reject, resolve), 19 | }); 20 | 21 | Future.of = x => Future((reject, resolve) => resolve(x)); 22 | Future.resolve = Future.of; 23 | Future.reject = err => Future(reject => reject(err)); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Asynchronicity-Too 2 | 3 | A repo of toy implementations of concurrency helpers. 4 | I tried to simplify code where possible, and there is way more English in these examples than I'm typically comfortable with; but they're toy examples, explanation is the whole point. 5 | 6 | I may consider adding less-commented, more succinct, or Typed examples, as well. 7 | 8 | ### Concurrency models under consideration: 9 | - [x] Futures 10 | - [x] Promises 11 | - [x] Observables 12 | - [ ] Streams (Node-like) 13 | - [ ] Fibers 14 | 15 | ### NOTE: 16 | This is not remotely intended for production. Not even a little. These aren't meant for performance or production. I feel that the abject aversion to closure in a lot of codebases is *crazy*, especially with the huge lengths developers will go to implement simple things using a more OO fashion (really, how many objects and proxy dispatches does it take to make up for a couple of closures?). But with all of that said, these are not performance tested, nor battle-hardened, and shall likely never be. 17 | 18 | If you decide to use them, knowing this: 19 | - caveat emptor 20 | - bon chance 21 | - this repo is an idea graveyard, which might see some flowers once in a while, but is otherwise eternally-still -------------------------------------------------------------------------------- /es6/observable.js: -------------------------------------------------------------------------------- 1 | const noop = () => {}; 2 | 3 | const Observable = subscribe => { 4 | let ended = false; // this is the gate to announce this link in the stream is done 5 | let cleanup = noop; // this is a function returned from subscribe, to do clean-up 6 | 7 | // check to see if the stream is done, before running the function 8 | const callIfNotEnded = f => x => { 9 | if (!ended) f(x); 10 | }; 11 | 12 | // close the stream if the function is called 13 | const endIfCalled = f => x => { 14 | ended = true; 15 | f(x); 16 | cleanup(); 17 | }; 18 | 19 | // the Observable returned to the user 20 | const observable = { 21 | // returns a new Observable, which when subscribed to, will trigger subscription to this observable 22 | map: f => 23 | Observable( 24 | observer => 25 | observable.subscribe( 26 | x => observer.next(f(x)), // map over the value before moving downstream 27 | observer.error, 28 | observer.complete 29 | ).unsubscribe // give the next link the previous unsubscribe, to tell it when to stop running 30 | ), 31 | 32 | // returns a new observable, and f is expected to return new observables 33 | chain: f => 34 | Observable( 35 | observer => 36 | observable.subscribe( 37 | // call f(x) and subscribe to the observable that comes out 38 | // does not tie completion to any one observable, because many might be created 39 | // ensuring that all of the upstreams are cleaned up would be a *great* idea 40 | // but that would take the focus off of the intent of chain, in this example lib 41 | x => { 42 | f(x).subscribe(observer.next, observer.error); 43 | }, 44 | observer.error, 45 | observer.complete 46 | ).unsubscribe 47 | ), 48 | 49 | // triggers the provided `subscribe` to be run: lazy invocation 50 | subscribe: (onNext = noop, onError = noop, onComplete = noop) => { 51 | // wrap the given functions, to follow internal behaviour 52 | const next = callIfNotEnded(onNext); 53 | const error = callIfNotEnded(endIfCalled(onError)); 54 | const complete = callIfNotEnded(endIfCalled(onComplete)); 55 | 56 | // create an observer that is given to the initial subscription function 57 | const observer = { next, error, complete }; 58 | 59 | // call the Observable(subscribe) function, passing in the Observer 60 | // the subscription can return a cleanup function, which should be cached 61 | const subscriberCleanup = subscribe(observer); 62 | cleanup = 63 | typeof subscriberCleanup === "function" ? subscriberCleanup : noop; 64 | // create a subscription with an unsubscribe 65 | const subscription = { unsubscribe: complete }; 66 | return subscription; 67 | } 68 | }; 69 | 70 | if (Observable.extend) { 71 | Observable.extend(observable); 72 | } 73 | return observable; 74 | }; 75 | 76 | Observable.of = (...args) => 77 | Observable(observer => { 78 | args.forEach(observer.next); 79 | observer.complete(); 80 | }); 81 | 82 | // The rest of this file is add-ons and bonus features that might make the API more extensible for different environments, and usecases 83 | 84 | // convert from an array/iterable to an observable 85 | Observable.from = iterable => Observable.of(...iterable); 86 | 87 | // convert from a promise to an observable 88 | Observable.fromPromise = promise => 89 | Observable(observer => { 90 | promise.then( 91 | x => { 92 | observer.next(x); 93 | observer.complete(); 94 | }, 95 | err => observer.error(err) 96 | ); 97 | }); 98 | 99 | // convert observable stream to a promise of an array of data 100 | Observable.toPromise = observable => 101 | new Promise((resolve, reject) => { 102 | const results = []; 103 | observable.subscribe(x => results.push(x), reject, () => resolve(results)); 104 | }); 105 | 106 | // subscribe to, and unsubscribe from, DOM events 107 | Observable.fromEvent = (type, el) => 108 | Observable(observer => { 109 | el.addEventListener(type, observer.next); 110 | return () => { 111 | console.log("Unbinding"); 112 | el.removeEventListener(type, observer.next); 113 | }; 114 | }); 115 | 116 | // examples of how to extend observables with all of the helper methods found in Rx or Bacon 117 | Observable.extend = observable => { 118 | for (let key in Observable.extensions) { 119 | observable[key] = Observable.extensions[key](observable); 120 | } 121 | }; 122 | 123 | // a place for storing extensions to register observables with 124 | // note, these are all "Bonus" or opt-in behaviours. "Rx" is "Reactive Extensions", as it were 125 | // to be useful as near-monads, Observables really just need map and chain 126 | Observable.extensions = { 127 | 128 | // subscribes with a mutation to run for each "next" call; returns a promise that resolves or rejects with the stream 129 | forEach: observable => mutate => 130 | new Promise((resolve, reject) => 131 | observable.subscribe(mutate, reject, resolve) 132 | ), 133 | 134 | // exactly like map, but instead of transforming, only passes values through if they pass the predicate 135 | filter: observable => predicate => 136 | Observable( 137 | observer => 138 | observable.subscribe( 139 | x => { 140 | if (predicate(x)) { 141 | observer.next(x); 142 | } 143 | }, 144 | observer.error, 145 | observer.complete 146 | ).unsubscribe 147 | ), 148 | 149 | // a straight passthrough (`map(x => x)`) but will close the stream after n items pass through 150 | take: observable => remaining => 151 | Observable( 152 | observer => 153 | observable.subscribe( 154 | x => { 155 | if (remaining) { 156 | observer.next(x); 157 | remaining -= 1; 158 | } else { 159 | observer.complete(); 160 | } 161 | }, 162 | observer.error, 163 | observer.complete 164 | ).unsubscribe 165 | ), 166 | 167 | // like take, but will take while a predicate is truthy 168 | takeWhile: observable => predicate => 169 | Observable( 170 | observer => 171 | observable.subscribe( 172 | x => { 173 | if (predicate(x)) { 174 | observer.next(x); 175 | } else { 176 | observer.complete(); 177 | } 178 | }, 179 | observer.error, 180 | observer.complete 181 | ).unsubscribe 182 | ), 183 | 184 | // like take, but will take until a predicate is truthy 185 | takeUntil: observable => predicate => 186 | Observable( 187 | observer => 188 | observable.subscribe(x => { 189 | if (predicate(x)) { 190 | observer.complete(); 191 | } else { 192 | observer.next(x); 193 | } 194 | }).unsubscribe 195 | ) 196 | }; 197 | -------------------------------------------------------------------------------- /es6/promise.js: -------------------------------------------------------------------------------- 1 | const PENDING = "pending"; 2 | const RESOLVED = "resolved"; 3 | const REJECTED = "rejected"; 4 | 5 | function Promise(initialize) { 6 | if (!(this instanceof Promise)) { 7 | throw new TypeError( 8 | "This is a bad requirement, but I'm doing it for accuracy." 9 | ); 10 | } 11 | if (typeof initialize !== "function") { 12 | throw new ReferenceError( 13 | `A Promise must be provided a function; got ${typeof initialize}` 14 | ); 15 | } 16 | 17 | let state = PENDING; // PENDING | RESOLVED | REJECTED 18 | let value = null; // the value or the error that comes back 19 | 20 | let listeners = []; // the list of all callers of `then` *before* the promise is settled 21 | 22 | // this gets passed into initialize `new Promise((onResolve) => ...)` 23 | const onResolve = result => { 24 | if (state !== PENDING) return; // only fire once 25 | let then = getThen(result); // get then, if result is a promise 26 | if (then) { 27 | // resolve the internal state, based on the result promise resolving 28 | then.call(result, resolvePromise, rejectPromise); 29 | } else { 30 | // resolve the internal state 31 | resolvePromise(result); 32 | } 33 | }; 34 | 35 | // this gets passed into initialize `new Promise((_, onReject) => ...)` 36 | // I'm not sure why it doesn't resolve promises... but that's how it works 37 | const onReject = err => { 38 | if (state !== PENDING) return; // only fire once 39 | rejectPromise(err); // resolve the internal state with the error 40 | }; 41 | 42 | // this is a helper function, to return `.then` if result is a promise 43 | // if result has magic getters, this is to get the value once and cache it 44 | const getThen = result => { 45 | const then = result && result.then; // checking that result exists, first 46 | if (typeof then === "function") { 47 | return then; // only return `then` if it's a function 48 | } 49 | }; 50 | 51 | // this is what sets the internal state and finalizes the promise 52 | // and triggers the promise chain 53 | const resolvePromise = result => { 54 | if (state !== PENDING) return; // only fire once 55 | state = RESOLVED; 56 | value = result; 57 | notifyListeners(); 58 | }; 59 | 60 | // this is what sets the internal state and finalizes the promise 61 | // and triggers the promise chain 62 | const rejectPromise = err => { 63 | if (state !== PENDING) return; // only fire once 64 | state = REJECTED; 65 | value = err; 66 | notifyListeners(); 67 | }; 68 | 69 | // this will trigger the resolution of chained promises 70 | const notifyListener = listener => { 71 | if (state === RESOLVED) { 72 | listener.resolve(value); 73 | } else if (state === REJECTED) { 74 | listener.reject(value); 75 | } 76 | }; 77 | 78 | const notifyListeners = () => { 79 | setTimeout(() => { 80 | listeners.forEach(notifyListener); 81 | listeners = []; // flush the listeners for GC purposes 82 | }, 0); 83 | }; 84 | 85 | // this contains the Promise/A+ compliant behaviour for running `.then(onSuccess)` 86 | // and resolving/rejecting the next promise in the chain with the result 87 | const handleSuccessChain = (value, onSuccess, resolveNext, rejectNext) => { 88 | if (!onSuccess) { 89 | // handles the case of `.then(null, err => ...)` 90 | resolveNext(value); // just pass the value on to the subscriber 91 | } else { 92 | try { 93 | const result = onSuccess(value); // run `.then(onSuccess)` 94 | const then = getThen(result); // dereference `.then` (it might be a getter, so cache it) 95 | if (then) { 96 | // if result is "Thenable" 97 | // the `.then` the user called will subscribe to the promise we just got back 98 | // `.then` has `call` applied, to solve the `this` problem 99 | then.call(result, resolveNext, rejectNext); 100 | } else { 101 | // if the result is not "Thenable", pass it to the `.then(onSuccess)` that was called 102 | resolveNext(result); 103 | } 104 | } catch (err) { 105 | // if anything above goes wrong, throw into the `.then(null, onError)` that was called 106 | rejectNext(err); 107 | } 108 | } 109 | }; 110 | 111 | // this contains the Promise/A+ compliant behaviour for running `.then(null, onError)` 112 | // and resolving/rejecting the next promise in the chain with the result 113 | const handleErrorChain = (value, onError, resolveNext, rejectNext) => { 114 | if (!onError) { 115 | // handles the case of `.then(onSuccess)` 116 | rejectNext(value); // just pass the value on to the subscriber 117 | } else { 118 | try { 119 | const result = onError(value); // run `.then(null, onError)` 120 | resolveNext(result); 121 | } catch (err) { 122 | // if anything above goes wrong, throw into the `.then(null, onError)` that was called 123 | rejectNext(err); 124 | } 125 | } 126 | }; 127 | 128 | // Note that the two functions above could be simplified in a pretty straightforward way, and lower the LoC and mental overhead 129 | // but there are subtle differences between the two paths, because of how error handling works; I wanted to make sure people could follow that 130 | 131 | // a "Listener" is almost like an inverted promise (like `q.deferred`) 132 | // this is an implementation-specific thing, no end user sees, but is how 133 | // I chose to keep track of managing the complex `onSuccess`/`onError` handling 134 | // instead of keeping it all inside of the `then` 135 | const makeListener = (onSuccess, onError, resolveNext, rejectNext) => ({ 136 | // this resolve represents the success path for a promise 137 | resolve: value => 138 | handleSuccessChain(value, onSuccess, resolveNext, rejectNext), 139 | // this resolve represents the failure path for a promise 140 | reject: err => handleErrorChain(value, onError, resolveNext, rejectNext) 141 | }); 142 | 143 | // this is the "Thenable" given to the end user 144 | const thenable = { 145 | then: (onSuccess, onError) => { 146 | // immediately return a new promise, to keep the chain going 147 | return new Promise((resolveNext, rejectNext) => { 148 | // this listener will manage resolving this new promise, when the outer promise resolves 149 | // again, the listener is implementation-specific, but the resolution is compliant 150 | const listener = makeListener( 151 | onSuccess, 152 | onError, 153 | resolveNext, 154 | rejectNext 155 | ); 156 | if (state === "pending") { 157 | // we're still waiting 158 | listeners.push(listener); // add to the list of waiting listeners 159 | } else { 160 | // set a timout (always async, as per spec) and run the resolver 161 | setTimeout(() => notifyListener(listener), 0); 162 | } 163 | }); 164 | }, 165 | // `.catch` is just a shorthand 166 | catch: onError => thenable.then(null, onError) 167 | }; 168 | 169 | // now that we've defined all of the state and all of the resolution for the promise... 170 | // IMMEDIATELY AND SYNCHRONOUSLY fire the function that you were given 171 | // and pass in the functions that will set the state and value, and trigger the listeners 172 | try { 173 | initialize(onResolve, onReject); 174 | } catch (err) { 175 | // if anything throws inside of the initial function, 176 | // immediately throw into the `.then(null, onError)` 177 | onReject(err); 178 | } 179 | 180 | return thenable; // et voilà! 181 | } 182 | 183 | Promise.resolve = value => new Promise(resolve => resolve(value)); 184 | 185 | Promise.reject = err => new Promise((_, reject) => reject(err)); 186 | 187 | // takes an array of promises and returns one promise, resolved with the results, 188 | // or rejected with the first error to fire 189 | Promise.all = promises => { 190 | let remaining = promises.length; 191 | let results = Array.from({ length: promises.length }); // empty array as long as the list 192 | 193 | return new Promise((resolve, reject) => { 194 | promises.forEach((value, i) => { 195 | Promise.resolve(value) // resolve in case it's not a promise 196 | .then( 197 | result => { 198 | results[i] = result; // fill the same slot in the array with the result 199 | remaining -= 1; 200 | if (!remaining) { 201 | // if we're done, return the new list 202 | resolve(results); 203 | } 204 | }, 205 | // trigger a rejection of the whole outer promise, if any fail 206 | reject 207 | ); 208 | }); 209 | }); 210 | }; 211 | 212 | // takes an array of promises and resolves or rejects with the first value to come back 213 | Promise.race = promises => 214 | new Promise((resolve, reject) => 215 | promises.forEach(value => Promise.resolve(value).then(resolve, reject)) 216 | ); 217 | --------------------------------------------------------------------------------