Skip to content

Instantly share code, notes, and snippets.

@qntm
Last active September 15, 2020 20:14
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save qntm/0270e5df5ce5b2057a0e035a8c889e67 to your computer and use it in GitHub Desktop.
Save qntm/0270e5df5ce5b2057a0e035a8c889e67 to your computer and use it in GitHub Desktop.
Promises/A+ implementation in 300 bytes
let P=exports.P=(t,e,r=t=>typeof t==typeof P,c,o=t=>c=t,h=(t,e)=>{try{e=t(e),l==e?c():e===Object(e)&&r(t=e.then)?t.call(e,t=>e&&h(e=>t,e=0),t=>e&&o([t],e=0)):o([e,1])}catch(t){o([t])}},l={r:(c,P)=>setTimeout(l=>r(P=c[1]?t:e)?h(P,c[0]):o(c)),then:(t,e,r=P(t,e),h=o)=>(c?r.r(c):o=t=>r.r(t,h(t)),r)})=>l
/**
Hand-cranked version of `broken-promise` (q.v.) intended to actually work and to be easier to minify.
One general method used repeatedly in this module is avoiding the need for `var`, `let` or `const`
declarations by just sticking arguments with default values in the nearest enclosing function's
argument list. E.g. `()=>{var x=7;...}` becomes `(x=7)=>{...}`.
Another method is to deliberately return values from a function whose return value is ignored,
saving braces, e.g. `()=>{d(e,f)}` becomes `()=>d(e,f)`.
Frustrations: it seems to cost bytes to make it so you do `promise(value)` to resolve rather than
`promise.r(value)`, because assigning the `.then` method to `promise` takes an extra line of code.
Also, setting default values for `onFulfilled` and `onRejected`, so that in `setTimeout` we can
just always call `RESOLVE`, costs bytes overall.
*/
// `settleEverything` is called once more to fulfill
// than to reject, which means it's cheapest if REJECT is "omitted" (`undefined`).
// This variable is elided.
const FULFILLED = 1
// The method has a very short name for minification reasons but here it's a
// big variable so the source is more legible. This variable is elided.
const SETTLE_DEFERRED_METHOD_NAME = 'r'
let P = exports.P = (
// The only two relevant arguments are `onFulfilled` and `onRejected`.
onFulfilled,
onRejected,
// Fake arguments from here down
isFunction = x => typeof x == typeof P, // i.e. "function"
// A promise can be pending, fulfilled or rejected.
// `combinedState` is either `undefined` (indicating that the promise is pending)
// or a two-entry array [value, FULFILLED] or a one-entry array
// [reason], implying REJECTED.
combinedState,
// This function manually settles the current promise and all deferred child
// promises. Initially it just does `promise` but later its value is replaced
// with new functions which call this one and then do more work.
// This function is (or should be) guarded, so that it can only
// ever be called once and so we can't fulfill with a thenable/promise.
// Return value is ignored
settleEverything = newCombinedState => {
return combinedState = newCombinedState
},
// The Promise Resolution Procedure.
// Figure out how to settle promise `self`, given a value `x` to settle it
// with. In general, if `x` is a regular value then we resolve with `x`.
// If `x` is another promise then we settle THE SAME WAY AS `x`.
// If anything goes wrong then we reject with the error.
RESOLVE = (
// This argument is dual-usage, it's `onSettled` and later `then`.
onSettledOrThen/*onSettled*/,
// This argument is used for THREE distinct purposes. At first it is
// `newValue`, then it gets reused as `x`, and finally it is `unsettled`,
// which is truthy at first (because it was previously `x`, a thenable)
// but then gets set to 0 to indicate that `self` has settled.
newValueOrXOrUnsettled/*newValue*/
) => {
try {
// This should not be part of the Promise Resolution Procedure but
// handling it here allows us to go down to just one try/catch block
newValueOrXOrUnsettled/*x*/ = onSettledOrThen/*onSettled*/(newValueOrXOrUnsettled/*newValue*/)
if (promise == newValueOrXOrUnsettled/*x*/) {
// The spec says we need to throw a TypeError in this case but
// hilariously it is not able to specify WHAT TypeError should be
// thrown. So we can actually just throw any old TypeError. The
// shortest way I can think of to do this is to try to call
// a local variable which is not actually a function.
combinedState()
} else if (
newValueOrXOrUnsettled/*x*/ === Object(newValueOrXOrUnsettled/*x*/) // i.e. `x` is not a primitive. This has to be a triple equals
&& isFunction(onSettledOrThen/*then*/ = newValueOrXOrUnsettled/*x*/.then)
) {
onSettledOrThen/*then*/.call(
newValueOrXOrUnsettled/*x*/,
// Return value here is not significant
// TODO: factor out?
y => newValueOrXOrUnsettled/*unsettled*/ && (
RESOLVE(
_ => y,
newValueOrXOrUnsettled/*unsettled*/ = 0 // parameter gets ignored, just moving this statement to save bytes
)
),
// Return value here is not significant
r => newValueOrXOrUnsettled/*unsettled*/ && (
settleEverything(
[r],
newValueOrXOrUnsettled/*unsettled*/ = 0 // parameter gets ignored, just moving this statement to save bytes
)
)
)
} else {
settleEverything([newValueOrXOrUnsettled/*x*/, FULFILLED])
}
} catch (e) {
// This happens if `onSettled` throws, if self == `x`,
// if getting `x.then` throws an exception,
// or if calling `x.then()` throws an exception
settleEverything([e])
}
},
// This is the actual promise object which will be returned once this
// constructor is done with.
promise = {
// This is the public method the consumer calls to resolve or reject this promise.
// If this promise is a deferred child of a parent promise, then this method
// will be called when that parent promise settles or, if we were created
// attached to an already-settled promise, immediately on creation.
//
// Strictly speaking there should be some kind of conditional in here which
// makes sure that consumers cannot try to manually force the same promise to
// settle twice, but SURPRISE, this interface is up to the implementation
// which means the Promises/A+ test suite cannot, and does not, test it,
// so we can save some bytes!
//
// The value returned from this method is ignored.
[SETTLE_DEFERRED_METHOD_NAME]: (
newCombinedState,
// Fake argument
onSettled
) => setTimeout(_ => {
// Return values are ignored
if (isFunction(onSettled = newCombinedState[1] ? onFulfilled : onRejected)) {
return RESOLVE(onSettled, newCombinedState[0])
} else {
return settleEverything(newCombinedState)
}
}),
// Call this method on this promise to be returned a new promise which
// transforms the result from the old.
then: (
onFulfilled,
onRejected,
// Fake arguments
promise2 = P(onFulfilled, onRejected),
prevSettleEverything = settleEverything
) => {
if (combinedState) {
promise2[SETTLE_DEFERRED_METHOD_NAME](combinedState)
} else {
// Return value is ignored
settleEverything = newCombinedState => {
return promise2[SETTLE_DEFERRED_METHOD_NAME](
newCombinedState,
// This argument is ignored (well, fake/overwritten).
// Just using it to do some work here instead of in a preceding line of code
// to save bytes.
prevSettleEverything(newCombinedState)
)
}
}
return promise2
}
}
) => promise
exports.deferred=(e=require(".").P())=>({promise:e,resolve:r=>e.r([r,1]),reject:r=>e.r([r])})
const promisesAplusTests = require("promises-aplus-tests")
const adapter = require('./the_adapter.js')
promisesAplusTests(adapter, {bail: true}, function(err) {
if(err) {
console.error(err)
}
})
@qntm
Copy link
Author

qntm commented May 27, 2018

P

This is P, a tiny ("golfed") implementation of the Promises/A+ specification, originally based on broken-promises-aplus. This is the smallest compliant implementation I can find. If you can find a smaller one, let me know!

Usage

The API is, of course, rather golfed too:

const P = require('.').P

// Create a promise
const promise = P()

// Cause the promise to resolve
promise.r(['value', true])

// Or cause the promise to reject
promise.r([Error('reason'), false])

Testing

A full adapter object, for passing to promises-aplus-tests, is provided as the_adapter.js. (Not adapter.js because I want index.js to be first alphabetically so it appears in the Gist summary.)

Philosophy

Constraints of this project:

  • The code has to work in whatever JavaScript environment I'm using right now. For me that's Node.js v8.9.0 which means all kinds of ES6 constructs are cheerfully available, but not e.g. ES6 module syntax. No interest in any particular web browser.
  • No interfering with the normal running of the Promises/A+ test suite, using e.g. volkswagen or techniques similar to those used by volkswagen.
  • No just pulling in the engine's existing Promises/A+ implementation if any i.e. module.exports = Promise, ho ho ho.
  • Just to be absolute: no pulling in third-party dependencies of any kind.
  • No modifying the caller's objects.
  • No disobeying the spec just because it's impossible for the test suite to test everything. E.g. 'bind'in x is not an acceptable way to test whether an object x is a function, no matter how many bytes it saves over typeof x=='function'.
  • The test harness the_test.js is a fixture, it just pulls in adapter.js and passes it directly to promises-aplus-tests. No shenanigans are possible there, and that code is not part of the byte count.
  • Most subjectively: the adapter, although it should be also be minimal, may be a separate piece of code which is not counted towards the implementation's overall byte count. In theory it would be possible to lodge the entire implementation inside adapter.js, reducing index.js to an empty string. In some early attempts at this challenge I actually counted the byte count of both implementation and adapter, which resulted in me combining the two files and golfing the combination. But eventually I changed my mind, and the new, highly subjective rule is that adapter.js should also contain "no shenanigans" - which implies that the implementation should expose a reasonably practical, usable API. Define this how you like.

Future

I think the ultimate stopping point for me, the gold standard, would be if this implementation could be made to fit into a Tweet (280 characters) without any kind of compression. (Note that it already fits comfortably if encoded using e.g. base2048.)

I do think this is technically possible, but I am very much running out of ideas now. So, such a thing may be out of reach at least for me and for variations of this particular implementation. It may take a whole new perspective and starting point.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment