UNPKG

13.7 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.Engine = void 0;
4const tslib_1 = require("tslib");
5const plugin_utils_1 = require("@remixproject/plugin-utils");
6class Engine {
7 constructor() {
8 this.plugins = {};
9 this.events = {};
10 this.listeners = {};
11 this.eventMemory = {};
12 }
13 /**
14 * Broadcast an event to the plugin listening
15 * @param emitter Plugin name that emits the event
16 * @param event The name of the event
17 * @param payload The content of the event
18 */
19 broadcast(emitter, event, ...payload) {
20 const eventName = plugin_utils_1.listenEvent(emitter, event);
21 if (!this.listeners[eventName])
22 return; // Nobody is listening
23 const listeners = this.listeners[eventName] || [];
24 listeners.forEach((listener) => {
25 if (!this.events[listener][eventName]) {
26 throw new Error(`Plugin ${listener} should be listening on event ${event} from ${emitter}. But no callback have been found`);
27 }
28 this.events[listener][eventName](...payload);
29 });
30 // Add event memory
31 this.eventMemory[emitter]
32 ? this.eventMemory[emitter][event] = payload
33 : this.eventMemory[emitter] = { [event]: payload };
34 }
35 /**
36 * Start listening on an event from another plugin
37 * @param listener The name of the plugin that listen on the event
38 * @param emitter The name of the plugin that emit the event
39 * @param event The name of the event
40 * @param cb Callback function to trigger when the event is trigger
41 */
42 addListener(listener, emitter, event, cb) {
43 const eventName = plugin_utils_1.listenEvent(emitter, event);
44 // If not already listening
45 if (!this.events[listener][eventName]) {
46 this.events[listener][eventName] = cb;
47 }
48 // Record that "listener" is listening on "emitter"
49 if (!this.listeners[eventName])
50 this.listeners[eventName] = [];
51 // If not already recorded
52 if (!this.listeners[eventName].includes(listener)) {
53 this.listeners[eventName].push(listener);
54 }
55 // If engine has memory of this event emit previous value
56 if (emitter in this.eventMemory && event in this.eventMemory[emitter]) {
57 cb(...this.eventMemory[emitter][event]);
58 }
59 }
60 /**
61 * Remove an event from the list of a listener's events
62 * @param listener The name of the plugin that was listening on the event
63 * @param emitter The name of the plugin that emitted the event
64 * @param event The name of the event
65 */
66 removeListener(listener, emitter, event) {
67 const eventName = plugin_utils_1.listenEvent(emitter, event);
68 // Remove listener
69 this.listeners[eventName] = this.listeners[eventName].filter(name => name !== listener);
70 // Remove callback
71 delete this.events[listener][eventName];
72 }
73 /**
74 * Create a listener that listen only once on an event
75 * @param listener The name of the plugin that listen on the event
76 * @param emitter The name of the plugin that emitted the event
77 * @param event The name of the event
78 * @param cb Callback function to trigger when event is triggered
79 */
80 listenOnce(listener, emitter, event, cb) {
81 this.addListener(listener, emitter, event, (...args) => {
82 cb(...args);
83 this.removeListener(listener, emitter, event);
84 });
85 }
86 /**
87 * Call a method of a plugin from another
88 * @param caller The name of the plugin that calls the method
89 * @param path The path of the plugin that manages the method
90 * @param method The name of the method
91 * @param payload The argument to pass to the method
92 */
93 callMethod(caller, path, method, ...payload) {
94 var _a;
95 return tslib_1.__awaiter(this, void 0, void 0, function* () {
96 const target = path.split('.').shift();
97 if (!this.plugins[target]) {
98 throw new Error(`Cannot call ${target} from ${caller}, because ${target} is not registered`);
99 }
100 // Get latest version of the profiles
101 const [to, from] = yield Promise.all([
102 this.manager.getProfile(target),
103 this.manager.getProfile(caller),
104 ]);
105 // Check if plugin FROM can activate plugin TO
106 const isActive = yield this.manager.isActive(target);
107 if (!isActive) {
108 const managerCanActivate = yield this.manager.canActivatePlugin(from, to, method);
109 const pluginCanActivate = yield ((_a = this.plugins[to.name]) === null || _a === void 0 ? void 0 : _a.canActivate(to, method));
110 if (managerCanActivate && pluginCanActivate) {
111 yield this.manager.toggleActive(target);
112 }
113 else {
114 throw new Error(`${from.name} cannot call ${method} of ${target}, because ${target} is not activated yet`);
115 }
116 }
117 // Check if method is exposed
118 // note: native methods go here
119 const methods = [...(to.methods || []), 'canDeactivate'];
120 if (!methods.includes(method)) {
121 const notExposedMsg = `Cannot call method "${method}" of "${target}" from "${caller}", because "${method}" is not exposed.`;
122 const exposedMethodsMsg = `Here is the list of exposed methods: ${methods.map(m => `"${m}"`).join(',')}`;
123 throw new Error(`${notExposedMsg} ${exposedMethodsMsg}`);
124 }
125 const request = { from: caller, path };
126 return this.plugins[target]['addRequest'](request, method, payload);
127 });
128 }
129 /**
130 * Create an object to easily access any registered plugin
131 * @param name Name of the caller plugin
132 * @note This method creates a snapshot at the time of activation
133 */
134 createApp(name) {
135 return tslib_1.__awaiter(this, void 0, void 0, function* () {
136 const getProfiles = Object.keys(this.plugins).map(key => this.manager.getProfile(key));
137 const profiles = yield Promise.all(getProfiles);
138 return profiles.reduce((app, target) => {
139 app[target.name] = (target.methods || []).reduce((methods, method) => {
140 methods[method] = (...payload) => this.callMethod(name, target.name, method, ...payload);
141 return methods;
142 }, {
143 on: (event, cb) => this.addListener(name, target.name, event, cb),
144 once: (event, cb) => this.listenOnce(name, target.name, event, cb),
145 off: (event) => this.removeListener(name, target.name, event),
146 profile: target
147 });
148 return app;
149 }, {});
150 });
151 }
152 /**
153 * Activate a plugin by making its method and event available
154 * @param name The name of the plugin
155 * @note This method is trigger by the plugin manager when a plugin has been activated
156 */
157 activatePlugin(name) {
158 return tslib_1.__awaiter(this, void 0, void 0, function* () {
159 if (!this.plugins[name]) {
160 throw new Error(`Cannot active plugin ${name} because it's not registered yet`);
161 }
162 const isActive = yield this.manager.isActive(name);
163 if (isActive)
164 return;
165 const plugin = this.plugins[name];
166 this.events[name] = {};
167 plugin['on'] = (emitter, event, cb) => {
168 this.addListener(name, emitter, event, cb);
169 };
170 plugin['once'] = (emitter, event, cb) => {
171 this.listenOnce(name, emitter, event, cb);
172 };
173 plugin['off'] = (emitter, event) => {
174 this.removeListener(name, emitter, event);
175 };
176 plugin['emit'] = (event, ...payload) => {
177 this.broadcast(name, event, ...payload);
178 };
179 plugin['call'] = (target, method, ...payload) => {
180 return this.callMethod(name, target, method, ...payload);
181 };
182 // GIVE ACCESS TO APP
183 plugin['app'] = yield this.createApp(name);
184 plugin['createApp'] = () => this.createApp(name);
185 // Call hooks
186 yield plugin.activate();
187 });
188 }
189 /**
190 * Deactivate a plugin by removing all its event listeners and making it inaccessible
191 * @param name The name of the plugin
192 * @note This method is trigger by the plugin manager when a plugin has been deactivated
193 */
194 deactivatePlugin(name) {
195 return tslib_1.__awaiter(this, void 0, void 0, function* () {
196 if (!this.plugins[name]) {
197 throw new Error(`Cannot deactive plugin ${name} because it's not registered yet`);
198 }
199 const isActive = yield this.manager.isActive(name);
200 if (!isActive)
201 return;
202 const plugin = this.plugins[name];
203 // Call hooks
204 yield plugin.deactivate();
205 this.updateErrorHandler(plugin);
206 // REMOVE PLUGIN APP
207 delete plugin['app'];
208 delete plugin['createApp'];
209 // REMOVE LISTENER
210 // Note : We don't remove the listeners of this plugin.
211 // Because we would keep track of them to reactivate them on reactivation. Which doesn't make sense
212 delete this.events[name];
213 // Remove event memory from this plugin
214 delete this.eventMemory[name];
215 // REMOVE EVENT RECORD
216 Object.keys(this.listeners).forEach(eventName => {
217 this.listeners[eventName].forEach((listener, i) => {
218 if (listener === name)
219 this.listeners[eventName].splice(i, 1);
220 });
221 });
222 });
223 }
224 /**
225 * Update error message when trying to call a method when not activated
226 * @param plugin The deactivated plugin to update the methods from
227 */
228 updateErrorHandler(plugin) {
229 const name = plugin.name;
230 // SET ERROR MESSAGE FOR call, on, once, off, emit
231 const deactivatedWarning = (message) => {
232 return `Plugin "${name}" is currently deactivated. ${message}. Activate "${name}" first.`;
233 };
234 plugin['call'] = (target, key, ...payload) => {
235 throw new Error(deactivatedWarning(`It cannot call method ${key} of plugin ${target}.`));
236 };
237 plugin['on'] = (target, event) => {
238 throw new Error(deactivatedWarning(`It cannot listen on event ${event} of plugin ${target}.`));
239 };
240 plugin['once'] = (target, event) => {
241 throw new Error(deactivatedWarning(`It cannot listen on event ${event} of plugin ${target}.`));
242 };
243 plugin['off'] = (target, event) => {
244 throw new Error(deactivatedWarning('All event listeners are already removed.'));
245 };
246 plugin['emit'] = (event, ...payload) => {
247 throw new Error(deactivatedWarning(`It cannot emit the event ${event}`));
248 };
249 }
250 /**
251 * Register a plugin to the engine and update the manager
252 * @param plugin The plugin
253 */
254 register(plugins) {
255 const register = (plugin) => {
256 var _a;
257 if (this.plugins[plugin.name]) {
258 throw new Error(`Plugin ${plugin.name} is already register.`);
259 }
260 if (plugin.name === 'manager') {
261 this.registerManager(plugin);
262 }
263 this.plugins[plugin.name] = plugin;
264 (_a = this.manager) === null || _a === void 0 ? void 0 : _a.addProfile(plugin.profile);
265 // Update Error Handling for better debug
266 this.updateErrorHandler(plugin);
267 // SetPluginOption is before onRegistration to let plugin update it's option inside onRegistration
268 if (this.setPluginOption) {
269 const options = this.setPluginOption(plugin.profile);
270 plugin.setOptions(options);
271 }
272 if (plugin.onRegistration)
273 plugin.onRegistration();
274 if (this.onRegistration)
275 this.onRegistration(plugin);
276 return plugin.name;
277 };
278 return Array.isArray(plugins) ? plugins.map(register) : register(plugins);
279 }
280 /** Register the manager */
281 registerManager(manager) {
282 this.manager = manager;
283 // Activate the Engine & start listening on activation and deactivation
284 this.manager['engineActivatePlugin'] = (name) => this.activatePlugin(name);
285 this.manager['engineDeactivatePlugin'] = (name) => this.deactivatePlugin(name);
286 // Add all previous profiles
287 const profiles = Object.values(this.plugins).map(p => p.profile);
288 this.manager.addProfile(profiles);
289 }
290 /** Remove plugin(s) from engine */
291 remove(names) {
292 const remove = (name) => tslib_1.__awaiter(this, void 0, void 0, function* () {
293 yield this.manager.deactivatePlugin(name);
294 delete this.listeners[name];
295 delete this.plugins[name];
296 });
297 return Array.isArray(names)
298 ? Promise.all(names.map(remove))
299 : remove(names);
300 }
301 /**
302 * Check is a name is already registered
303 * @param name Name of the plugin
304 */
305 isRegistered(name) {
306 return !!this.plugins[name];
307 }
308}
309exports.Engine = Engine;
310//# sourceMappingURL=engine.js.map
\No newline at end of file