From 90646745ee38465f4e35e1f6f73781bfad37e2ea Mon Sep 17 00:00:00 2001 From: Alec Gibson <12036746+alecgibson@users.noreply.github.com> Date: Thu, 24 Jul 2025 13:50:18 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Allow=20sorting=20middleware?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit At the moment middleware is executed in the order that it is registered. This is fine if middleware is only registered in a single place, but in distributed setups, there may be multiple places in code (possibly across repos) that consumers may want to register middleware. In order to allow some distributed way of defining an execution order of middlewares, this is a non-breaking change that lets consumers define an `order` for their middleware when registering: ```js backend.use('commit', fn, 10); ``` This `order` will be used to sort the middlewares, such that higher `order` execute after middleware with lower `order`. Ties are broken on registration order (the current behaviour), since JavaScript's `.sort()` [is stable][1]. The default value for `order` is `0`, so if consumers want to run middleware before other middleware that has not specified an `order`, they will need to set a negative `order` (which is legal): ```js backend.use('commit', fn, -10); ``` [1]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#sort_stability --- docs/api/backend.md | 9 ++++ lib/backend.js | 8 ++- test/middleware.js | 119 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 2 deletions(-) diff --git a/docs/api/backend.md b/docs/api/backend.md index b3efa8b04..724149574 100644 --- a/docs/api/backend.md +++ b/docs/api/backend.md @@ -251,6 +251,15 @@ backend.use(action, middleware) > A [middleware]({{ site.baseurl }}{% link middleware/index.md %}) function +`order` -- number + +Optional +{: .label .label-grey } + +> Default: `0` + +> The order to run this middleware relative to other middlewares. Middleware with higher `order` will run later. Ties are broken on registration order. + ### addProjection() Defines a [projection]({{ site.baseurl }}{% link projections.md %}). diff --git a/lib/backend.js b/lib/backend.js index dcc95aa33..c3d409bd9 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -183,15 +183,19 @@ Backend.prototype.addProjection = function(name, collection, fields) { /** * Add middleware to an action or array of actions */ -Backend.prototype.use = function(action, fn) { +Backend.prototype.use = function(action, fn, order) { if (Array.isArray(action)) { for (var i = 0; i < action.length; i++) { - this.use(action[i], fn); + this.use(action[i], fn, order); } return this; } + fn.__order = order || 0; var fns = this.middleware[action] || (this.middleware[action] = []); fns.push(fn); + fns.sort(function(a, b) { + return a.__order - b.__order; + }); return this; }; diff --git a/test/middleware.js b/test/middleware.js index 66e50b2fa..7c73c2a65 100644 --- a/test/middleware.js +++ b/test/middleware.js @@ -6,6 +6,7 @@ var errorHandler = util.errorHandler; var ShareDBError = require('../lib/error'); var sinon = require('sinon'); var ACTIONS = require('../lib/message-actions').ACTIONS; +var async = require('async'); var ERROR_CODE = ShareDBError.CODES; @@ -35,6 +36,124 @@ describe('middleware', function() { var response = this.backend.use(['submit', 'connect'], function() {}); expect(response).equal(this.backend); }); + + describe('sorting', function() { + var calls; + + beforeEach(function() { + calls = []; + }); + + it('defaults to insertion order', function(done) { + var a = makeMiddleware(); + var b = makeMiddleware(); + var c = makeMiddleware(); + + this.backend.use('readSnapshots', a); + this.backend.use('readSnapshots', b); + this.backend.use('readSnapshots', c); + + var connection = this.backend.connect(); + var doc = connection.get('dogs', 'fido'); + + async.series([ + doc.fetch.bind(doc), + function(next) { + expect(calls).to.eql([a, b, c]); + next(); + } + ], done); + }); + + it('sorts middleware by order', function(done) { + var a = makeMiddleware(); + var b = makeMiddleware(); + var c = makeMiddleware(); + + this.backend.use('readSnapshots', a, 2); + this.backend.use('readSnapshots', b, 1); + this.backend.use('readSnapshots', c, 3); + + var connection = this.backend.connect(); + var doc = connection.get('dogs', 'fido'); + + async.series([ + doc.fetch.bind(doc), + function(next) { + expect(calls).to.eql([b, a, c]); + next(); + } + ], done); + }); + + it('defaults order to 0', function(done) { + var a = makeMiddleware(); + var b = makeMiddleware(); + var c = makeMiddleware(); + + this.backend.use('readSnapshots', a); + this.backend.use('readSnapshots', b, 1); + this.backend.use('readSnapshots', c, -1); + + var connection = this.backend.connect(); + var doc = connection.get('dogs', 'fido'); + + async.series([ + doc.fetch.bind(doc), + function(next) { + expect(calls).to.eql([c, a, b]); + next(); + } + ], done); + }); + + it('can sort using MAX_SAFE_INTEGER and MIN_SAFE_INTEGER', function(done) { + var a = makeMiddleware(); + var b = makeMiddleware(); + + this.backend.use('readSnapshots', a, Number.MAX_SAFE_INTEGER); + this.backend.use('readSnapshots', b, Number.MIN_SAFE_INTEGER); + + var connection = this.backend.connect(); + var doc = connection.get('dogs', 'fido'); + + async.series([ + doc.fetch.bind(doc), + function(next) { + expect(calls).to.eql([b, a]); + next(); + } + ], done); + }); + + + it('can sort using MAX_VALUE', function(done) { + var a = makeMiddleware(); + var b = makeMiddleware(); + + this.backend.use('readSnapshots', a, Number.MAX_VALUE); + this.backend.use('readSnapshots', b, -Number.MAX_VALUE); + + var connection = this.backend.connect(); + var doc = connection.get('dogs', 'fido'); + + async.series([ + doc.fetch.bind(doc), + function(next) { + expect(calls).to.eql([b, a]); + next(); + } + ], done); + }); + + function makeMiddleware() { + var fn = function(context, next) { + calls.push(fn); + next(); + }; + return fn; + } + }); }); describe('connect', function() {