Skip to content

Commit fb10efc

Browse files
committed
feat(context): add events to ContextView
Extension points that use ContextView can then listen on events to update its state/cache beyond the view accordingly.
1 parent c3c5dab commit fb10efc

File tree

3 files changed

+104
-4
lines changed

3 files changed

+104
-4
lines changed

docs/site/Context.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,3 +492,41 @@ the context with `listen()`.
492492
If your dependency needs to follow the context for values from bindings matching
493493
a filter, use [`@inject.view`](Decorators_inject.md#@inject.view) for dependency
494494
injection.
495+
496+
### ContextView events
497+
498+
A `ContextView` object can emit one of the following events:
499+
500+
- 'refresh': when the view is refreshed as bindings are added/removed
501+
- 'resolve': when the cached values are resolved and updated
502+
- 'close': when the view is closed (stopped observing context events)
503+
504+
Such as events can be used to update other states/cached values other than the
505+
values watched by the `ContextView` object itself. For example:
506+
507+
```ts
508+
class MyController {
509+
private _total: number | undefined = undefined;
510+
constructor(
511+
@inject.view(filterByTag('counter'))
512+
private taggedAsFoo: ContextView<Counter>,
513+
) {
514+
// Invalidate cached `_total` if the view is refreshed
515+
taggedAsFoo.on('refresh', () => {
516+
this._total = undefined;
517+
});
518+
}
519+
520+
async total() {
521+
if (this._total != null) return this._total;
522+
// Calculate the total of all counters
523+
const counters = await this.taggedAsFoo.values();
524+
let result = 0;
525+
for (const c of counters) {
526+
result += c.value;
527+
}
528+
this._total = result;
529+
return this._total;
530+
}
531+
}
532+
```

packages/context/src/__tests__/unit/context-view.unit.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,49 @@ describe('ContextView', () => {
9090
expect(await taggedAsFoo.values()).to.eql(['BAR', 'FOO']);
9191
});
9292

93+
describe('EventEmitter', () => {
94+
let events: string[] = [];
95+
96+
beforeEach(setupListeners);
97+
98+
it('emits close', () => {
99+
taggedAsFoo.close();
100+
expect(events).to.eql(['close']);
101+
// 2nd close does not emit `close` as it's closed
102+
taggedAsFoo.close();
103+
expect(events).to.eql(['close']);
104+
});
105+
106+
it('emits refresh', () => {
107+
taggedAsFoo.refresh();
108+
expect(events).to.eql(['refresh']);
109+
});
110+
111+
it('emits resolve', async () => {
112+
await taggedAsFoo.values();
113+
expect(events).to.eql(['resolve']);
114+
// Second call does not resolve as values are cached
115+
await taggedAsFoo.values();
116+
expect(events).to.eql(['resolve']);
117+
});
118+
119+
it('emits refresh & resolve when bindings are changed', async () => {
120+
server
121+
.bind('xyz')
122+
.to('XYZ')
123+
.tag('foo');
124+
await taggedAsFoo.values();
125+
expect(events).to.eql(['refresh', 'resolve']);
126+
});
127+
128+
function setupListeners() {
129+
events = [];
130+
['open', 'close', 'refresh', 'resolve'].forEach(t =>
131+
taggedAsFoo.on(t, () => events.push(t)),
132+
);
133+
}
134+
});
135+
93136
function givenContextView() {
94137
bindings = [];
95138
givenContext();

packages/context/src/context-view.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
// License text available at https://opensource.org/licenses/MIT
55

66
import * as debugFactory from 'debug';
7+
import {EventEmitter} from 'events';
78
import {promisify} from 'util';
89
import {Binding} from './binding';
910
import {BindingFilter} from './binding-filter';
@@ -28,23 +29,34 @@ const nextTick = promisify(process.nextTick);
2829
* points. For example, the RestServer can react to `controller` bindings even
2930
* they are added/removed/updated after the application starts.
3031
*
32+
* `ContextView` is an event emitter that emits the following events:
33+
* - 'close': when the view is closed (stopped observing context events)
34+
* - 'refresh': when the view is refreshed as bindings are added/removed
35+
* - 'resolve': when the cached values are resolved and updated
3136
*/
32-
export class ContextView<T = unknown> implements ContextObserver {
37+
export class ContextView<T = unknown> extends EventEmitter
38+
implements ContextObserver {
3339
protected _cachedBindings: Readonly<Binding<T>>[] | undefined;
3440
protected _cachedValues: T[] | undefined;
3541
private _subscription: Subscription | undefined;
3642

3743
constructor(
3844
protected readonly context: Context,
3945
public readonly filter: BindingFilter,
40-
) {}
46+
) {
47+
super();
48+
}
4149

4250
/**
4351
* Start listening events from the context
4452
*/
4553
open() {
4654
debug('Start listening on changes of context %s', this.context.name);
47-
return (this._subscription = this.context.subscribe(this));
55+
if (this.context.isSubscribed(this)) {
56+
return this._subscription;
57+
}
58+
this._subscription = this.context.subscribe(this);
59+
return this._subscription;
4860
}
4961

5062
/**
@@ -55,6 +67,7 @@ export class ContextView<T = unknown> implements ContextObserver {
5567
if (!this._subscription || this._subscription.closed) return;
5668
this._subscription.unsubscribe();
5769
this._subscription = undefined;
70+
this.emit('close');
5871
}
5972

6073
/**
@@ -92,6 +105,7 @@ export class ContextView<T = unknown> implements ContextObserver {
92105
debug('Refreshing the view by invalidating cache');
93106
this._cachedBindings = undefined;
94107
this._cachedValues = undefined;
108+
this.emit('refresh');
95109
}
96110

97111
/**
@@ -105,9 +119,14 @@ export class ContextView<T = unknown> implements ContextObserver {
105119
return b.getValue(this.context, ResolutionSession.fork(session));
106120
});
107121
if (isPromiseLike(result)) {
108-
result = result.then(values => (this._cachedValues = values));
122+
result = result.then(values => {
123+
this._cachedValues = values;
124+
this.emit('resolve', values);
125+
return values;
126+
});
109127
} else {
110128
this._cachedValues = result;
129+
this.emit('resolve', result);
111130
}
112131
return result;
113132
}

0 commit comments

Comments
 (0)