UNPKG

9.4 kBPlain TextView Raw
1import Reflux, { Store as RefluxStore } from 'reflux';
2import EventEmitter from 'eventemitter3';
3import { Actions } from './actions';
4
5/**
6 * A non-magic number that is still small and higher than
7 * the number of components registered for a single role
8 * that we would expect.
9 */
10const INT8_MAX = 127;
11
12/**
13 * Returning a fake store when asking for a store that does not
14 * exist.
15 */
16const STUB_STORE = Reflux.createStore({});
17
18interface Role {
19 name: string;
20 component: React.JSXElementConstructor<unknown>;
21 order?: number;
22}
23
24interface Store extends RefluxStore {
25 onActivated?: (appRegistry: AppRegistry) => void;
26}
27
28/**
29 * Is a registry for all user interface components, stores, and actions
30 * in the application.
31 */
32export class AppRegistry {
33 _emitter: EventEmitter;
34 actions: Record<string, unknown>;
35 components: Record<string, React.JSXElementConstructor<unknown>>;
36 stores: Record<string, Store>;
37 roles: Record<string, Role[]>;
38 storeMisses: Record<string, number>;
39
40 /**
41 * Instantiate the registry.
42 */
43 constructor() {
44 this._emitter = new EventEmitter();
45 this.actions = {};
46 this.components = {};
47 this.stores = {};
48 this.roles = {};
49 this.storeMisses = {};
50 }
51
52 // Helper until this module is 'proper' fake ESM
53 static get Actions(): typeof Actions {
54 return Actions;
55 }
56
57 // Helper until this module is 'proper' fake ESM
58 static get AppRegistry(): typeof AppRegistry {
59 return AppRegistry;
60 }
61
62 /**
63 * Deregister an Actions.
64 *
65 * @param {String} name - The action to deregister.
66 *
67 * @returns {AppRegistry} This instance.
68 */
69 deregisterAction(name: string): this {
70 delete this.actions[name];
71 Actions.actionDeregistered(name);
72 return this;
73 }
74
75 /**
76 * Deregister a component.
77 *
78 * @param {String} name - The component to deregister.
79 *
80 * @returns {AppRegistry} This instance.
81 */
82 deregisterComponent(name: string): this {
83 delete this.components[name];
84 Actions.componentDeregistered(name);
85 return this;
86 }
87
88 /**
89 * Deregister a role.
90 *
91 * @param {String} name - The role name.
92 * @param {Object} object - The role to deregister.
93 *
94 * @returns {AppRegistry} This instance.
95 */
96 deregisterRole(name: string, object: Role): this {
97 const roles = this.roles[name];
98 roles.splice(roles.indexOf(object), 1);
99 Actions.roleDeregistered(name);
100 return this;
101 }
102
103 /**
104 * Deregister a store.
105 *
106 * @param {String} name - The store to deregister.
107 *
108 * @returns {AppRegistry} This instance.
109 */
110 deregisterStore(name: string): this {
111 delete this.stores[name];
112 Actions.storeDeregistered(name);
113 return this;
114 }
115
116 /**
117 * Get an action for the name.
118 *
119 * @param {String} name - The action name.
120 *
121 * @returns {Action} The Actions.
122 */
123 getAction(name: string): unknown {
124 return this.actions[name];
125 }
126
127 /**
128 * Get a component by name.
129 *
130 * @param {String} name - The component name.
131 *
132 * @returns {Component} The component.
133 */
134 getComponent(name: string): React.JSXElementConstructor<unknown> | undefined {
135 return this.components[name];
136 }
137
138 /**
139 * Get a role by name.
140 *
141 * @param {String} name - The role name.
142 *
143 * @returns {Array} The role components.
144 */
145 getRole(name: string): Role[] | undefined {
146 return this.roles[name];
147 }
148
149 /**
150 * Get a store by name.
151 *
152 * @param {String} name - The store name.
153 *
154 * @returns {Store} The store.
155 */
156 getStore(name: string): Store {
157 const store = this.stores[name];
158 if (store === undefined) {
159 this.storeMisses[name] = (this.storeMisses[name] || 0) + 1;
160 return STUB_STORE;
161 }
162 return store;
163 }
164
165 /**
166 * Calls onActivated on all the stores in the registry.
167 *
168 * @returns {AppRegistry} The app registry.
169 */
170 onActivated(): this {
171 return this._callOnStores((store) => {
172 if (store.onActivated) {
173 store.onActivated(this);
174 }
175 });
176 }
177
178 /**
179 * Register an action in the registry.
180 *
181 * @param {String} name - The name of the Actions.
182 * @param {Action} action - The Actions.
183 *
184 * @returns {AppRegistry} This instance.
185 */
186 registerAction(name: string, action: unknown): this {
187 const overwrite = Object.prototype.hasOwnProperty.call(this.actions, name);
188 this.actions[name] = action;
189 if (overwrite) {
190 Actions.actionOverridden(name);
191 } else {
192 Actions.actionRegistered(name);
193 }
194 return this;
195 }
196
197 /**
198 * Register a component in the registry.
199 *
200 * @param {String} name - The name of the component.
201 * @param {Component} component - The React Component.
202 *
203 * @returns {AppRegistry} This instance.
204 */
205 registerComponent(
206 name: string,
207 component: React.JSXElementConstructor<unknown>
208 ): this {
209 const overwrite = Object.prototype.hasOwnProperty.call(
210 this.components,
211 name
212 );
213 this.components[name] = component;
214 if (overwrite) {
215 Actions.componentOverridden(name);
216 } else {
217 Actions.componentRegistered(name);
218 }
219 return this;
220 }
221
222 /**
223 * Register a role.
224 *
225 * @param {String} name - The role name.
226 * @param {Object} role - The role object.
227 *
228 * @returns {AppRegistry} This instance.
229 */
230 registerRole(name: string, role: Role): this {
231 if (
232 Object.prototype.hasOwnProperty.call(this.roles, name) &&
233 !this.roles[name].includes(role)
234 ) {
235 this.roles[name].push(role);
236 this.roles[name].sort(this._roleComparator.bind(this));
237 } else {
238 this.roles[name] = [role];
239 }
240 Actions.roleRegistered(name);
241 return this;
242 }
243
244 /**
245 * Register a store in the registry.
246 *
247 * @param {String} name - The name of the store.
248 * @param {Store} store - The Reflux store.
249 *
250 * @returns {AppRegistry} This instance.
251 */
252 registerStore(name: string, store: Store): this {
253 const overwrite = Object.prototype.hasOwnProperty.call(this.stores, name);
254 this.stores[name] = store;
255 if (overwrite) {
256 Actions.storeOverridden(name);
257 } else {
258 Actions.storeRegistered(name);
259 }
260 return this;
261 }
262
263 /**
264 * Adds a listener for the event name to the underlying event emitter.
265 *
266 * @param {String} eventName - The event name.
267 * @param {Function} listener - The listener.
268 *
269 * @returns {AppRegistry} The chainable app registry.
270 */
271 addListener(eventName: string, listener: (...args: any[]) => void): this {
272 return this.on(eventName, listener);
273 }
274
275 /**
276 * Emits an event for the name with the provided arguments.
277 *
278 * @param {String} eventName - The event name.
279 * @param {...Object} args - The arguments.
280 *
281 * @returns {Boolean} If the event had listeners.
282 */
283 emit(eventName: string, ...args: any[]): boolean {
284 return this._emitter.emit(eventName, ...args);
285 }
286
287 /**
288 * Return all the event names.
289 *
290 * @returns {Array} The event names.
291 */
292 eventNames(): string[] {
293 return this._emitter.eventNames() as string[];
294 }
295
296 /**
297 * Gets a count of listeners for the event name.
298 *
299 * @param {String} eventName - The event name.
300 *
301 * @returns {Number} The listener count.
302 */
303 listenerCount(eventName: string): number {
304 return this._emitter.listeners(eventName).length;
305 }
306
307 /**
308 * Get all the listeners for the event.
309 *
310 * @param {String} eventName - The event name.
311 *
312 * @returns {Array} The listeners for the event.
313 */
314 listeners(eventName: string): ((...args: any[]) => void)[] {
315 return this._emitter.listeners(eventName);
316 }
317
318 /**
319 * Adds a listener for the event name to the underlying event emitter.
320 *
321 * @param {String} eventName - The event name.
322 * @param {Function} listener - The listener.
323 *
324 * @returns {AppRegistry} The chainable app registry.
325 */
326 on(eventName: string, listener: (...args: any[]) => void): this {
327 this._emitter.on(eventName, listener);
328 return this;
329 }
330
331 /**
332 * Adds a listener for the event name to the underlying event emitter
333 * to handle an event only once.
334 *
335 * @param {String} eventName - The event name.
336 * @param {Function} listener - The listener.
337 *
338 * @returns {AppRegistry} The chainable app registry.
339 */
340 once(eventName: string, listener: (...args: any[]) => void): this {
341 this._emitter.once(eventName, listener);
342 return this;
343 }
344
345 /**
346 * Removes a listener for the event.
347 *
348 * @param {String} eventName - The event name.
349 * @param {Function} listener - The listener.
350 *
351 * @returns {AppRegistry} The chainable app registry.
352 */
353 removeListener(eventName: string, listener: (...args: any[]) => void): this {
354 this._emitter.removeListener(eventName, listener);
355 return this;
356 }
357
358 /**
359 * Removes all the listeners for the event name.
360 *
361 * @param {String} eventName - The event name.
362 *
363 * @returns {AppRegistry} The chainable app registry.
364 */
365 removeAllListeners(eventName: string): this {
366 this._emitter.removeAllListeners(eventName);
367 return this;
368 }
369
370 _callOnStores(fn: (store: Store) => void): this {
371 for (const key of Object.keys(this.stores)) {
372 const store = this.stores[key];
373 fn(store);
374 }
375 return this;
376 }
377
378 _roleComparator(a: Role, b: Role): number {
379 const aOrder = a.order || INT8_MAX;
380 const bOrder = b.order || INT8_MAX;
381 return aOrder - bOrder;
382 }
383}
384
\No newline at end of file