UNPKG

8.01 kBPlain TextView Raw
1// Copyright IBM Corp. and LoopBack contributors 2018,2020. All Rights Reserved.
2// Node module: @loopback/core
3// This file is licensed under the MIT License.
4// License text available at https://opensource.org/licenses/MIT
5
6import {
7 Binding,
8 Context,
9 ContextView,
10 inject,
11 invokeMethod,
12 sortBindingsByPhase,
13} from '@loopback/context';
14import debugFactory from 'debug';
15import {CoreBindings, CoreTags} from './keys';
16import {LifeCycleObserver, lifeCycleObserverFilter} from './lifecycle';
17const debug = debugFactory('loopback:core:lifecycle');
18
19/**
20 * A group of life cycle observers
21 */
22export type LifeCycleObserverGroup = {
23 /**
24 * Observer group name
25 */
26 group: string;
27 /**
28 * Bindings for observers within the group
29 */
30 bindings: Readonly<Binding<LifeCycleObserver>>[];
31};
32
33export type LifeCycleObserverOptions = {
34 /**
35 * Control the order of observer groups for notifications. For example,
36 * with `['datasource', 'server']`, the observers in `datasource` group are
37 * notified before those in `server` group during `start`. Please note that
38 * observers are notified in the reverse order during `stop`.
39 */
40 orderedGroups: string[];
41 /**
42 * Override and disable lifecycle observer groups. This setting applies to
43 * both ordered groups (i.e. those defined in `orderedGroups`) and unordered
44 * groups.
45 */
46 disabledGroups?: string[];
47 /**
48 * Notify observers of the same group in parallel, default to `true`
49 */
50 parallel?: boolean;
51};
52
53export const DEFAULT_ORDERED_GROUPS = ['server'];
54
55/**
56 * A context-based registry for life cycle observers
57 */
58export class LifeCycleObserverRegistry implements LifeCycleObserver {
59 constructor(
60 @inject.context()
61 protected readonly context: Context,
62 @inject.view(lifeCycleObserverFilter)
63 protected readonly observersView: ContextView<LifeCycleObserver>,
64 @inject(CoreBindings.LIFE_CYCLE_OBSERVER_OPTIONS, {optional: true})
65 protected readonly options: LifeCycleObserverOptions = {
66 parallel: true,
67 orderedGroups: DEFAULT_ORDERED_GROUPS,
68 },
69 ) {}
70
71 setOrderedGroups(groups: string[]) {
72 this.options.orderedGroups = groups;
73 }
74
75 /**
76 * Get observer groups ordered by the group
77 */
78 public getObserverGroupsByOrder(): LifeCycleObserverGroup[] {
79 const bindings = this.observersView.bindings;
80 const groups = this.sortObserverBindingsByGroup(bindings);
81 if (debug.enabled) {
82 debug(
83 'Observer groups: %j',
84 groups.map(g => ({
85 group: g.group,
86 bindings: g.bindings.map(b => b.key),
87 })),
88 );
89 }
90 return groups;
91 }
92
93 /**
94 * Get the group for a given life cycle observer binding
95 * @param binding - Life cycle observer binding
96 */
97 protected getObserverGroup(
98 binding: Readonly<Binding<LifeCycleObserver>>,
99 ): string {
100 // First check if there is an explicit group name in the tag
101 let group = binding.tagMap[CoreTags.LIFE_CYCLE_OBSERVER_GROUP];
102 if (!group) {
103 // Fall back to a tag that matches one of the groups
104 group = this.options.orderedGroups.find(g => binding.tagMap[g] === g);
105 }
106 group = group || '';
107 debug(
108 'Binding %s is configured with observer group %s',
109 binding.key,
110 group,
111 );
112 return group;
113 }
114
115 /**
116 * Sort the life cycle observer bindings so that we can start/stop them
117 * in the right order. By default, we can start other observers before servers
118 * and stop them in the reverse order
119 * @param bindings - Life cycle observer bindings
120 */
121 protected sortObserverBindingsByGroup(
122 bindings: Readonly<Binding<LifeCycleObserver>>[],
123 ) {
124 // Group bindings in a map
125 const groupMap: Map<string, Readonly<Binding<LifeCycleObserver>>[]> =
126 new Map();
127 sortBindingsByPhase(
128 bindings,
129 CoreTags.LIFE_CYCLE_OBSERVER_GROUP,
130 this.options.orderedGroups,
131 );
132 for (const binding of bindings) {
133 const group = this.getObserverGroup(binding);
134 let bindingsInGroup = groupMap.get(group);
135 if (bindingsInGroup == null) {
136 bindingsInGroup = [];
137 groupMap.set(group, bindingsInGroup);
138 }
139 bindingsInGroup.push(binding);
140 }
141 // Create an array for group entries
142 const groups: LifeCycleObserverGroup[] = [];
143 for (const [group, bindingsInGroup] of groupMap) {
144 groups.push({group, bindings: bindingsInGroup});
145 }
146 return groups;
147 }
148
149 /**
150 * Notify an observer group of the given event
151 * @param group - A group of bindings for life cycle observers
152 * @param event - Event name
153 */
154 protected async notifyObservers(
155 observers: LifeCycleObserver[],
156 bindings: Readonly<Binding<LifeCycleObserver>>[],
157 event: keyof LifeCycleObserver,
158 ) {
159 if (!this.options.parallel) {
160 let index = 0;
161 for (const observer of observers) {
162 debug(
163 'Invoking %s observer for binding %s',
164 event,
165 bindings[index].key,
166 );
167 index++;
168 await this.invokeObserver(observer, event);
169 }
170 return;
171 }
172
173 // Parallel invocation
174 const notifiers = observers.map((observer, index) => {
175 debug('Invoking %s observer for binding %s', event, bindings[index].key);
176 return this.invokeObserver(observer, event);
177 });
178 await Promise.all(notifiers);
179 }
180
181 /**
182 * Invoke an observer for the given event
183 * @param observer - A life cycle observer
184 * @param event - Event name
185 */
186 protected async invokeObserver(
187 observer: LifeCycleObserver,
188 event: keyof LifeCycleObserver,
189 ) {
190 if (typeof observer[event] === 'function') {
191 // Supply `undefined` for legacy callback function expected by
192 // DataSource.stop()
193 await invokeMethod(observer, event, this.context, [undefined], {
194 skipInterceptors: true,
195 });
196 }
197 }
198
199 /**
200 * Emit events to the observer groups
201 * @param events - Event names
202 * @param groups - Observer groups
203 */
204 protected async notifyGroups(
205 events: (keyof LifeCycleObserver)[],
206 groups: LifeCycleObserverGroup[],
207 reverse = false,
208 ) {
209 const observers = await this.observersView.values();
210 const bindings = this.observersView.bindings;
211 const found = observers.some(observer =>
212 events.some(e => typeof observer[e] === 'function'),
213 );
214 if (!found) return;
215 if (reverse) {
216 // Do not reverse the original `groups` in place
217 groups = [...groups].reverse();
218 }
219 for (const group of groups) {
220 if (this.options.disabledGroups?.includes(group.group)) {
221 debug('Notification skipped (Group is disabled): %s', group.group);
222 continue;
223 }
224 const observersForGroup: LifeCycleObserver[] = [];
225 const bindingsInGroup = reverse
226 ? group.bindings.reverse()
227 : group.bindings;
228 for (const binding of bindingsInGroup) {
229 const index = bindings.indexOf(binding);
230 observersForGroup.push(observers[index]);
231 }
232
233 for (const event of events) {
234 debug('Beginning notification %s of %s...', event);
235 await this.notifyObservers(observersForGroup, group.bindings, event);
236 debug('Finished notification %s of %s', event);
237 }
238 }
239 }
240
241 /**
242 * Notify all life cycle observers by group of `init`
243 */
244 public async init(): Promise<void> {
245 debug('Initializing the %s...');
246 const groups = this.getObserverGroupsByOrder();
247 await this.notifyGroups(['init'], groups);
248 }
249
250 /**
251 * Notify all life cycle observers by group of `start`
252 */
253 public async start(): Promise<void> {
254 debug('Starting the %s...');
255 const groups = this.getObserverGroupsByOrder();
256 await this.notifyGroups(['start'], groups);
257 }
258
259 /**
260 * Notify all life cycle observers by group of `stop`
261 */
262 public async stop(): Promise<void> {
263 debug('Stopping the %s...');
264 const groups = this.getObserverGroupsByOrder();
265 // Stop in the reverse order
266 await this.notifyGroups(['stop'], groups, true);
267 }
268}