Skip to content

Commit

Permalink
feat(context): index bindings by tag to speed up matching by tag
Browse files Browse the repository at this point in the history
Fixes #4356

- create index for bindings by tag
- optimize find bindings by tag
- leverage findByTag for filterByTag to find matching bindings
  • Loading branch information
raymondfeng committed Jan 17, 2020
1 parent d7c966d commit e259f45
Show file tree
Hide file tree
Showing 6 changed files with 298 additions and 14 deletions.
Expand Up @@ -186,8 +186,8 @@ describe('Interceptor', () => {
}
}

// No listeners yet
expect(ctx.listenerCount('bind')).to.eql(0);
// No invocation context related listeners yet
const listenerCount = ctx.listenerCount('bind');
const controller = new MyController();

// Run the invocation 5 times
Expand All @@ -199,7 +199,7 @@ describe('Interceptor', () => {
'greet',
['John'],
);
// New listeners are added to `ctx`
// New listeners are added to `ctx` by the invocation context
expect(ctx.listenerCount('bind')).to.be.greaterThan(count);

// Wait until the invocation finishes
Expand All @@ -208,7 +208,7 @@ describe('Interceptor', () => {

// Listeners added by invocation context are gone now
// There is one left for ctx.observers
expect(ctx.listenerCount('bind')).to.eql(1);
expect(ctx.listenerCount('bind')).to.eql(listenerCount + 1);
});

it('invokes static interceptors', async () => {
Expand Down
22 changes: 20 additions & 2 deletions packages/context/src/__tests__/unit/context-view.unit.ts
Expand Up @@ -7,6 +7,7 @@ import {expect} from '@loopback/testlab';
import {
Binding,
BindingScope,
BindingTag,
compareBindingsByTag,
Context,
ContextView,
Expand All @@ -16,7 +17,7 @@ import {

describe('ContextView', () => {
let app: Context;
let server: Context;
let server: ServerContext;

let bindings: Binding<unknown>[];
let taggedAsFoo: ContextView;
Expand All @@ -27,6 +28,11 @@ describe('ContextView', () => {
expect(taggedAsFoo.bindings).to.eql(bindings);
});

it('leverages findByTag for binding tag filter', () => {
expect(taggedAsFoo.bindings).to.eql(bindings);
expect(server.findByTagInvoked).to.be.true();
});

it('sorts matched bindings', () => {
const view = new ContextView(
server,
Expand Down Expand Up @@ -199,9 +205,21 @@ describe('ContextView', () => {
taggedAsFoo = server.createView(filterByTag('foo'));
}

class ServerContext extends Context {
findByTagInvoked = false;
constructor(parent: Context, name: string) {
super(parent, name);
}

_findByTagIndex(tag: BindingTag) {
this.findByTagInvoked = true;
return super._findByTagIndex(tag);
}
}

function givenContext() {
app = new Context('app');
server = new Context(app, 'server');
server = new ServerContext(app, 'server');
bindings.push(
server
.bind('bar')
Expand Down
130 changes: 130 additions & 0 deletions packages/context/src/__tests__/unit/context.unit.ts
Expand Up @@ -9,9 +9,11 @@ import {
BindingCreationPolicy,
BindingKey,
BindingScope,
BindingTag,
BindingType,
Context,
ContextEventObserver,
filterByTag,
isPromiseLike,
Provider,
} from '../..';
Expand All @@ -32,6 +34,16 @@ class TestContext extends Context {
get eventListener() {
return this.parentEventListener;
}
get tagIndex() {
return this.bindingsIndexedByTag;
}

findByTagInvoked = false;

_findByTagIndex(tag: BindingTag | RegExp) {
this.findByTagInvoked = true;
return super._findByTagIndex(tag);
}
}

describe('Context constructor', () => {
Expand Down Expand Up @@ -94,6 +106,18 @@ describe('Context', () => {
'Cannot rebind key "foo" to a locked binding',
);
});

it('indexes a binding by tag', () => {
const binding = ctx.bind('foo').tag('a', {b: 1});
assertBindingIndexedByTag(binding, 'a', 'b');
});

it('indexes a binding by tag after being bound', () => {
const binding = ctx.bind('foo');
assertBindingNotIndexedByTag(binding, 'a', 'b');
binding.tag('a', {b: 1});
assertBindingIndexedByTag(binding, 'a', 'b');
});
});

describe('add', () => {
Expand All @@ -111,6 +135,20 @@ describe('Context', () => {
'Cannot rebind key "foo" to a locked binding',
);
});

it('indexes a binding by tag', () => {
const binding = new Binding('foo').to('bar').tag('a', {b: 1});
ctx.add(binding);
assertBindingIndexedByTag(binding, 'a', 'b');
});

it('indexes a binding by tag after being bound', () => {
const binding = new Binding('foo').to('bar');
ctx.add(binding);
assertBindingNotIndexedByTag(binding, 'a', 'b');
binding.tag('a', {b: 1});
assertBindingIndexedByTag(binding, 'a', 'b');
});
});

describe('contains', () => {
Expand Down Expand Up @@ -191,6 +229,16 @@ describe('Context', () => {
expect(result).to.be.false();
expect(ctx.contains('foo')).to.be.true();
});

it('removes indexes for a binding by tag', () => {
const binding = ctx
.bind('foo')
.to('bar')
.tag('a', {b: 1});
assertBindingIndexedByTag(binding, 'a', 'b');
ctx.unbind(binding.key);
assertBindingNotIndexedByTag(binding, 'a', 'b');
});
});

describe('find', () => {
Expand Down Expand Up @@ -276,6 +324,42 @@ describe('Context', () => {
result = ctx.find(binding => binding.tagNames.includes('b'));
expect(result).to.be.eql([b2, b3]);
});

it('leverages binding index by tag', () => {
ctx.bind('foo');
const b2 = ctx.bind('bar').tag('b');
const b3 = ctx.bind('baz').tag('b');
const result = ctx.find(filterByTag('b'));
expect(result).to.eql([b2, b3]);
expect(ctx.findByTagInvoked).to.be.true();
});

it('leverages binding index by tag wildcard', () => {
ctx.bind('foo');
const b2 = ctx.bind('bar').tag('b2');
const b3 = ctx.bind('baz').tag('b3');
const result = ctx.find(filterByTag('b?'));
expect(result).to.eql([b2, b3]);
expect(ctx.findByTagInvoked).to.be.true();
});

it('leverages binding index by tag regexp', () => {
ctx.bind('foo');
const b2 = ctx.bind('bar').tag('b2');
const b3 = ctx.bind('baz').tag('b3');
const result = ctx.find(filterByTag(/b\d/));
expect(result).to.eql([b2, b3]);
expect(ctx.findByTagInvoked).to.be.true();
});

it('leverages binding index by tag name/value pairs', () => {
ctx.bind('foo');
const b2 = ctx.bind('bar').tag({a: 1});
ctx.bind('baz').tag({a: 2, b: 1});
const result = ctx.find(filterByTag({a: 1}));
expect(result).to.eql([b2]);
expect(ctx.findByTagInvoked).to.be.true();
});
});

describe('findByTag with name pattern', () => {
Expand Down Expand Up @@ -316,6 +400,34 @@ describe('Context', () => {
expect(result).to.be.eql([b1]);
});

it('returns matching binding for multiple tags', () => {
const b1 = ctx
.bind('controllers.ProductController')
.tag({name: 'my-controller'})
.tag('controller');
ctx.bind('controllers.OrderController').tag('controller');
ctx.bind('dataSources.mysql').tag({dbType: 'mysql'});
const result = ctx.findByTag({
name: 'my-controller',
controller: 'controller',
});
expect(result).to.be.eql([b1]);
});

it('returns empty array if one of the tags does not match', () => {
ctx
.bind('controllers.ProductController')
.tag({name: 'my-controller'})
.tag('controller');
ctx.bind('controllers.OrderController').tag('controller');
ctx.bind('dataSources.mysql').tag({dbType: 'mysql'});
const result = ctx.findByTag({
controller: 'controller',
name: 'your-controller',
});
expect(result).to.be.eql([]);
});

it('returns empty array if no matching tag value is found', () => {
ctx.bind('controllers.ProductController').tag({name: 'my-controller'});
ctx.bind('controllers.OrderController').tag('controller');
Expand Down Expand Up @@ -888,4 +1000,22 @@ describe('Context', () => {
function createContext() {
ctx = new TestContext('app');
}

function assertBindingIndexedByTag(
binding: Binding<unknown>,
...tags: string[]
) {
for (const t of tags) {
expect(ctx.tagIndex.get(t)?.has(binding)).to.be.true();
}
}

function assertBindingNotIndexedByTag(
binding: Binding<unknown>,
...tags: string[]
) {
for (const t of tags) {
expect(!!ctx.tagIndex.get(t)?.has(binding)).to.be.false();
}
}
});

0 comments on commit e259f45

Please sign in to comment.