-
-
Save aronanda/d31bb47918145a5aace6005f172e035d to your computer and use it in GitHub Desktop.
class Middleware { | |
constructor(obj) { | |
obj = obj || this | |
Object.defineProperty(this, '__obj', { value: obj }) | |
Object.defineProperty(this, 'go', { value: function go(...args) { | |
args[args.length - 1].apply(obj, args.slice(0, -1)) | |
}, writable: true }) | |
} | |
use(fn) { | |
this.go = (stack => (...args) => stack(...args.slice(0, -1), () => | |
fn.call(this.__obj, ...args.slice(0, -1), args[args.length - 1] | |
.bind(this.__obj, ...args.slice(0, -1)))))(this.go) | |
return this | |
} | |
} |
class Middleware { | |
constructor(obj, method = 'go') { | |
let extras = [] | |
const stack = [] | |
const go = async (...args) => { | |
let done = args[args.length - 1] | |
if (typeof done === 'function') { | |
args.pop() | |
if (obj) done = done.bind(obj) | |
} else done = null | |
try { | |
for (let fn of stack) { | |
if (obj) fn = fn.bind(obj) | |
extras = await new Promise((resolve, reject) => { | |
fn(...args.concat(extras), function next(err, ...extras) { | |
if (err) return reject(err) | |
resolve(extras) | |
}) | |
}) | |
} | |
args = args.concat(extras) | |
if (done) done(...args) | |
return args | |
} catch (err) { throw err } | |
} | |
Object.defineProperty(this, method, { value: go }) | |
Object.defineProperty(this, 'use', { value: fn => stack.push(fn) }) | |
} | |
} |
const mw = new Middleware() | |
mw.use((a, b, next) => { | |
log('calling 1st func', { a, b }) | |
next() | |
}) | |
mw.use((a, b, next) => { | |
log('calling 2nd func', { a, b }) | |
next(null, 'c') | |
}) | |
mw.use((a, b, c, next) => { | |
log('calling 3rd func', { a, b, c }) | |
if (Math.random() > 0.5) | |
next(new Error('uh oh'), c) | |
else | |
next(null, c) | |
}) | |
mw.use((a, b, c, next) => { | |
log('calling 4th func', { a, b, c }) | |
next(null, c) | |
}) | |
try { | |
let result = await mw.go('a', 'b', (a, b, c) => log('done', { a, b, c })) | |
log(result) | |
} catch (err) { | |
log('err', err.toString()) | |
} |
const middleware = new Middleware(/* define object or defaults to this */) | |
middleware.use(function (req, res, next) { | |
setTimeout(() => { | |
this.hook1 = true | |
req.step++ | |
res.step++ | |
next() | |
}, 10) | |
}) | |
middleware.use(function (req, res, next) { | |
setTimeout(() => { | |
this.hook2 = true | |
req.step++ | |
res.step++ | |
next() | |
}, 10) | |
}).use((req, res, next) => { | |
// chainable | |
setTimeout(() => { | |
req.step++ | |
res.step++ | |
next() | |
}, 10) | |
}) | |
const start = new Date() | |
const req = Object.create(new class Req {}(), { step: { value: 1, enumerable: true, writable: true }}) | |
const res = Object.create(new class Res {}(), { step: { value: 1, enumerable: true, writable: true }}) | |
middleware.go(req, res, function (req, res) { | |
console.log(this) // Middleware { hook1: true, hook2: true } | |
console.log(req) // Req { step: 4 } | |
console.log(res) // Res { step: 4 } | |
console.log(new Date() - start) // around 30ms | |
}) |
I hear you w/ the brain f'cking -- I don't know how long I spent on wrapping my head around this, but it was long.
Basically, it's just chaining each next function together, when you call use(fn) it's making that fn the next next() : )
The arguments that go into the fn are arbitrary while the last argument will always be the 'next' callback.
That part with the ...args.slice(0, -1)
is basically taking your arbitrary arguments from the previous callback and putting them on the next callback (next()). You might be able to do a check there before passing the args to the "next" callback.
~ mind f*ky indeed )
perhaps something like this:
// inside use method
this.go = (stack => (...args) => stack(...args.slice(0, -1), () => {
const nextArgs = args.slice(0, -1)
const firstArg = nextArgs[0]
if (firstArg && firstArg instanceof Error)
throw firstArg
return fn.call(this.__obj, ...nextArgs, args[args.length - 1].bind(this.__obj, ...nextArgs))))(this.go)
}
return this
let me know what you figure out, I was just about to need that feature actually
I somehow hacked this piece of code together, without a fucking clue whats going on ;)
Added a "abortion" catch, when you pass a error argument to next
call.
When a "catcher" function is set, the catcher function gets called with the passed error and not the next use
callback.
And a other usefull feture, "overirde passed arguments".
use((A, B, C, next) => {
next(null, A, {data: true, ...B});
});
use((A, B, C, next) => {
// B is now a new object with the properties of original B
// And a new one "data"
console.log(A, B, C)
next();
});
The whole middleware class
class Middleware {
constructor(obj) {
this.catcher = null;
obj = obj || this;
Object.defineProperty(this, "__obj", {
value: obj
});
Object.defineProperty(this, "start", {
value: (...args) => {
let cb = args[args.length - 1];
let params = args.slice(0, -1);
cb.apply(obj, params);
},
writable: true
});
};
use(fn) {
this.start = (stack => {
return (...args) => {
stack(...args.slice(0, -1), (...override) => {
args.splice(0, override.length, ...override);
let next = (err, ...override) => {
if (err instanceof Error && this.catcher) {
return this.catcher(err);
} else if (!(err == undefined || err == null)) {
throw new Error(`First argument should be null/undefined or error instance, got: ${err}`);
}
args.splice(0, override.length, ...override);
args[args.length - 1].bind(this.__obj, ...args.slice(0, -1))();
};
fn.call(this.__obj, ...args.slice(0, -1), next);
});
};
})(this.start);
};
catch(fn) {
this.catcher = fn;
};
};
Not perfect, but serve my needs.
if you have any improvements, please let me know.
Nice, I see what you did there. Would be good to pass results to the following middlewares. Here's my latest solution that allows that and throws an error if one is present as the first argument of the next function. I used promises instead to make it easier to read (much easier). And I made the object binding optional as you oftentimes want to bind your own functions. Also, the result is returned at the end, regardless of whether you include a final callback in the go method (or rename the go method to something else ;)
class Middleware {
constructor(obj, method = 'go') {
let extras = []
const stack = []
const go = async (...args) => {
let done = args[args.length - 1]
if (typeof done === 'function') {
args.pop()
if (obj)
done = done.bind(obj)
} else
done = null
try {
for (let func of stack) {
if (obj)
func = func.bind(obj)
extras = await new Promise((resolve, reject) => {
func(...args.concat(extras), (err, ...extras) => {
if (err)
return reject(err)
resolve(extras)
})
})
}
args = args.concat(extras)
if (done)
done(...args)
return args
} catch (err) { throw err }
}
Object.defineProperty(this, method, { value: go })
Object.defineProperty(this, 'use', { value: fn => stack.push(fn) })
}
}
And here's a demo of my new version:
const mw = new Middleware()
mw.use((a, b, next) => {
log('calling 1st func', { a, b })
next()
})
mw.use((a, b, next) => {
log('calling 2nd func', { a, b })
next(null, 'c')
})
mw.use((a, b, c, next) => {
log('calling 3rd func', { a, b, c })
if (Math.random() > 0.5)
next(new Error('uh oh'), c)
else
next(null, c)
})
mw.use((a, b, c, next) => {
log('calling 4th func', { a, b, c })
next(null, c)
})
try {
let result = await mw.go('a', 'b', (a, b, c) => log('done', { a, b, c }))
log(result)
} catch (err) {
log('err', err.toString())
}
thanks for the nice work!
Wonderful. Perfect, exactly what im looking for while i found https://gist.github.com/darrenscerri/5c3b3dcbe4d370435cfa
Well done!
EDIT: Is there a explanation for this
use
function / stack creation.Im having trouble to understand how out of that the callback stack is created.
I want to add a "Abort" method, when a error is passed as first argument to the next function. While i try to understand that, it gives me a brain fu*k...