index.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  1. 'use strict';
  2. const {anyMap, producersMap, eventsMap} = require('./maps.js');
  3. const anyProducer = Symbol('anyProducer');
  4. const resolvedPromise = Promise.resolve();
  5. // Define symbols for "meta" events.
  6. const listenerAdded = Symbol('listenerAdded');
  7. const listenerRemoved = Symbol('listenerRemoved');
  8. let canEmitMetaEvents = false;
  9. let isGlobalDebugEnabled = false;
  10. function assertEventName(eventName) {
  11. if (typeof eventName !== 'string' && typeof eventName !== 'symbol' && typeof eventName !== 'number') {
  12. throw new TypeError('`eventName` must be a string, symbol, or number');
  13. }
  14. }
  15. function assertListener(listener) {
  16. if (typeof listener !== 'function') {
  17. throw new TypeError('listener must be a function');
  18. }
  19. }
  20. function getListeners(instance, eventName) {
  21. const events = eventsMap.get(instance);
  22. if (!events.has(eventName)) {
  23. return;
  24. }
  25. return events.get(eventName);
  26. }
  27. function getEventProducers(instance, eventName) {
  28. const key = typeof eventName === 'string' || typeof eventName === 'symbol' || typeof eventName === 'number' ? eventName : anyProducer;
  29. const producers = producersMap.get(instance);
  30. if (!producers.has(key)) {
  31. return;
  32. }
  33. return producers.get(key);
  34. }
  35. function enqueueProducers(instance, eventName, eventData) {
  36. const producers = producersMap.get(instance);
  37. if (producers.has(eventName)) {
  38. for (const producer of producers.get(eventName)) {
  39. producer.enqueue(eventData);
  40. }
  41. }
  42. if (producers.has(anyProducer)) {
  43. const item = Promise.all([eventName, eventData]);
  44. for (const producer of producers.get(anyProducer)) {
  45. producer.enqueue(item);
  46. }
  47. }
  48. }
  49. function iterator(instance, eventNames) {
  50. eventNames = Array.isArray(eventNames) ? eventNames : [eventNames];
  51. let isFinished = false;
  52. let flush = () => {};
  53. let queue = [];
  54. const producer = {
  55. enqueue(item) {
  56. queue.push(item);
  57. flush();
  58. },
  59. finish() {
  60. isFinished = true;
  61. flush();
  62. }
  63. };
  64. for (const eventName of eventNames) {
  65. let set = getEventProducers(instance, eventName);
  66. if (!set) {
  67. set = new Set();
  68. const producers = producersMap.get(instance);
  69. producers.set(eventName, set);
  70. }
  71. set.add(producer);
  72. }
  73. return {
  74. async next() {
  75. if (!queue) {
  76. return {done: true};
  77. }
  78. if (queue.length === 0) {
  79. if (isFinished) {
  80. queue = undefined;
  81. return this.next();
  82. }
  83. await new Promise(resolve => {
  84. flush = resolve;
  85. });
  86. return this.next();
  87. }
  88. return {
  89. done: false,
  90. value: await queue.shift()
  91. };
  92. },
  93. async return(value) {
  94. queue = undefined;
  95. for (const eventName of eventNames) {
  96. const set = getEventProducers(instance, eventName);
  97. if (set) {
  98. set.delete(producer);
  99. if (set.size === 0) {
  100. const producers = producersMap.get(instance);
  101. producers.delete(eventName);
  102. }
  103. }
  104. }
  105. flush();
  106. return arguments.length > 0 ?
  107. {done: true, value: await value} :
  108. {done: true};
  109. },
  110. [Symbol.asyncIterator]() {
  111. return this;
  112. }
  113. };
  114. }
  115. function defaultMethodNamesOrAssert(methodNames) {
  116. if (methodNames === undefined) {
  117. return allEmitteryMethods;
  118. }
  119. if (!Array.isArray(methodNames)) {
  120. throw new TypeError('`methodNames` must be an array of strings');
  121. }
  122. for (const methodName of methodNames) {
  123. if (!allEmitteryMethods.includes(methodName)) {
  124. if (typeof methodName !== 'string') {
  125. throw new TypeError('`methodNames` element must be a string');
  126. }
  127. throw new Error(`${methodName} is not Emittery method`);
  128. }
  129. }
  130. return methodNames;
  131. }
  132. const isMetaEvent = eventName => eventName === listenerAdded || eventName === listenerRemoved;
  133. function emitMetaEvent(emitter, eventName, eventData) {
  134. if (isMetaEvent(eventName)) {
  135. try {
  136. canEmitMetaEvents = true;
  137. emitter.emit(eventName, eventData);
  138. } finally {
  139. canEmitMetaEvents = false;
  140. }
  141. }
  142. }
  143. class Emittery {
  144. static mixin(emitteryPropertyName, methodNames) {
  145. methodNames = defaultMethodNamesOrAssert(methodNames);
  146. return target => {
  147. if (typeof target !== 'function') {
  148. throw new TypeError('`target` must be function');
  149. }
  150. for (const methodName of methodNames) {
  151. if (target.prototype[methodName] !== undefined) {
  152. throw new Error(`The property \`${methodName}\` already exists on \`target\``);
  153. }
  154. }
  155. function getEmitteryProperty() {
  156. Object.defineProperty(this, emitteryPropertyName, {
  157. enumerable: false,
  158. value: new Emittery()
  159. });
  160. return this[emitteryPropertyName];
  161. }
  162. Object.defineProperty(target.prototype, emitteryPropertyName, {
  163. enumerable: false,
  164. get: getEmitteryProperty
  165. });
  166. const emitteryMethodCaller = methodName => function (...args) {
  167. return this[emitteryPropertyName][methodName](...args);
  168. };
  169. for (const methodName of methodNames) {
  170. Object.defineProperty(target.prototype, methodName, {
  171. enumerable: false,
  172. value: emitteryMethodCaller(methodName)
  173. });
  174. }
  175. return target;
  176. };
  177. }
  178. static get isDebugEnabled() {
  179. if (typeof process !== 'object') {
  180. return isGlobalDebugEnabled;
  181. }
  182. const {env} = process || {env: {}};
  183. return env.DEBUG === 'emittery' || env.DEBUG === '*' || isGlobalDebugEnabled;
  184. }
  185. static set isDebugEnabled(newValue) {
  186. isGlobalDebugEnabled = newValue;
  187. }
  188. constructor(options = {}) {
  189. anyMap.set(this, new Set());
  190. eventsMap.set(this, new Map());
  191. producersMap.set(this, new Map());
  192. producersMap.get(this).set(anyProducer, new Set());
  193. this.debug = options.debug || {};
  194. if (this.debug.enabled === undefined) {
  195. this.debug.enabled = false;
  196. }
  197. if (!this.debug.logger) {
  198. this.debug.logger = (type, debugName, eventName, eventData) => {
  199. try {
  200. // TODO: Use https://github.com/sindresorhus/safe-stringify when the package is more mature. Just copy-paste the code.
  201. eventData = JSON.stringify(eventData);
  202. } catch {
  203. eventData = `Object with the following keys failed to stringify: ${Object.keys(eventData).join(',')}`;
  204. }
  205. if (typeof eventName === 'symbol' || typeof eventName === 'number') {
  206. eventName = eventName.toString();
  207. }
  208. const currentTime = new Date();
  209. const logTime = `${currentTime.getHours()}:${currentTime.getMinutes()}:${currentTime.getSeconds()}.${currentTime.getMilliseconds()}`;
  210. console.log(`[${logTime}][emittery:${type}][${debugName}] Event Name: ${eventName}\n\tdata: ${eventData}`);
  211. };
  212. }
  213. }
  214. logIfDebugEnabled(type, eventName, eventData) {
  215. if (Emittery.isDebugEnabled || this.debug.enabled) {
  216. this.debug.logger(type, this.debug.name, eventName, eventData);
  217. }
  218. }
  219. on(eventNames, listener) {
  220. assertListener(listener);
  221. eventNames = Array.isArray(eventNames) ? eventNames : [eventNames];
  222. for (const eventName of eventNames) {
  223. assertEventName(eventName);
  224. let set = getListeners(this, eventName);
  225. if (!set) {
  226. set = new Set();
  227. const events = eventsMap.get(this);
  228. events.set(eventName, set);
  229. }
  230. set.add(listener);
  231. this.logIfDebugEnabled('subscribe', eventName, undefined);
  232. if (!isMetaEvent(eventName)) {
  233. emitMetaEvent(this, listenerAdded, {eventName, listener});
  234. }
  235. }
  236. return this.off.bind(this, eventNames, listener);
  237. }
  238. off(eventNames, listener) {
  239. assertListener(listener);
  240. eventNames = Array.isArray(eventNames) ? eventNames : [eventNames];
  241. for (const eventName of eventNames) {
  242. assertEventName(eventName);
  243. const set = getListeners(this, eventName);
  244. if (set) {
  245. set.delete(listener);
  246. if (set.size === 0) {
  247. const events = eventsMap.get(this);
  248. events.delete(eventName);
  249. }
  250. }
  251. this.logIfDebugEnabled('unsubscribe', eventName, undefined);
  252. if (!isMetaEvent(eventName)) {
  253. emitMetaEvent(this, listenerRemoved, {eventName, listener});
  254. }
  255. }
  256. }
  257. once(eventNames) {
  258. let off_;
  259. const promise = new Promise(resolve => {
  260. off_ = this.on(eventNames, data => {
  261. off_();
  262. resolve(data);
  263. });
  264. });
  265. promise.off = off_;
  266. return promise;
  267. }
  268. events(eventNames) {
  269. eventNames = Array.isArray(eventNames) ? eventNames : [eventNames];
  270. for (const eventName of eventNames) {
  271. assertEventName(eventName);
  272. }
  273. return iterator(this, eventNames);
  274. }
  275. async emit(eventName, eventData) {
  276. assertEventName(eventName);
  277. if (isMetaEvent(eventName) && !canEmitMetaEvents) {
  278. throw new TypeError('`eventName` cannot be meta event `listenerAdded` or `listenerRemoved`');
  279. }
  280. this.logIfDebugEnabled('emit', eventName, eventData);
  281. enqueueProducers(this, eventName, eventData);
  282. const listeners = getListeners(this, eventName) || new Set();
  283. const anyListeners = anyMap.get(this);
  284. const staticListeners = [...listeners];
  285. const staticAnyListeners = isMetaEvent(eventName) ? [] : [...anyListeners];
  286. await resolvedPromise;
  287. await Promise.all([
  288. ...staticListeners.map(async listener => {
  289. if (listeners.has(listener)) {
  290. return listener(eventData);
  291. }
  292. }),
  293. ...staticAnyListeners.map(async listener => {
  294. if (anyListeners.has(listener)) {
  295. return listener(eventName, eventData);
  296. }
  297. })
  298. ]);
  299. }
  300. async emitSerial(eventName, eventData) {
  301. assertEventName(eventName);
  302. if (isMetaEvent(eventName) && !canEmitMetaEvents) {
  303. throw new TypeError('`eventName` cannot be meta event `listenerAdded` or `listenerRemoved`');
  304. }
  305. this.logIfDebugEnabled('emitSerial', eventName, eventData);
  306. const listeners = getListeners(this, eventName) || new Set();
  307. const anyListeners = anyMap.get(this);
  308. const staticListeners = [...listeners];
  309. const staticAnyListeners = [...anyListeners];
  310. await resolvedPromise;
  311. /* eslint-disable no-await-in-loop */
  312. for (const listener of staticListeners) {
  313. if (listeners.has(listener)) {
  314. await listener(eventData);
  315. }
  316. }
  317. for (const listener of staticAnyListeners) {
  318. if (anyListeners.has(listener)) {
  319. await listener(eventName, eventData);
  320. }
  321. }
  322. /* eslint-enable no-await-in-loop */
  323. }
  324. onAny(listener) {
  325. assertListener(listener);
  326. this.logIfDebugEnabled('subscribeAny', undefined, undefined);
  327. anyMap.get(this).add(listener);
  328. emitMetaEvent(this, listenerAdded, {listener});
  329. return this.offAny.bind(this, listener);
  330. }
  331. anyEvent() {
  332. return iterator(this);
  333. }
  334. offAny(listener) {
  335. assertListener(listener);
  336. this.logIfDebugEnabled('unsubscribeAny', undefined, undefined);
  337. emitMetaEvent(this, listenerRemoved, {listener});
  338. anyMap.get(this).delete(listener);
  339. }
  340. clearListeners(eventNames) {
  341. eventNames = Array.isArray(eventNames) ? eventNames : [eventNames];
  342. for (const eventName of eventNames) {
  343. this.logIfDebugEnabled('clear', eventName, undefined);
  344. if (typeof eventName === 'string' || typeof eventName === 'symbol' || typeof eventName === 'number') {
  345. const set = getListeners(this, eventName);
  346. if (set) {
  347. set.clear();
  348. }
  349. const producers = getEventProducers(this, eventName);
  350. if (producers) {
  351. for (const producer of producers) {
  352. producer.finish();
  353. }
  354. producers.clear();
  355. }
  356. } else {
  357. anyMap.get(this).clear();
  358. for (const [eventName, listeners] of eventsMap.get(this).entries()) {
  359. listeners.clear();
  360. eventsMap.get(this).delete(eventName);
  361. }
  362. for (const [eventName, producers] of producersMap.get(this).entries()) {
  363. for (const producer of producers) {
  364. producer.finish();
  365. }
  366. producers.clear();
  367. producersMap.get(this).delete(eventName);
  368. }
  369. }
  370. }
  371. }
  372. listenerCount(eventNames) {
  373. eventNames = Array.isArray(eventNames) ? eventNames : [eventNames];
  374. let count = 0;
  375. for (const eventName of eventNames) {
  376. if (typeof eventName === 'string') {
  377. count += anyMap.get(this).size + (getListeners(this, eventName) || new Set()).size +
  378. (getEventProducers(this, eventName) || new Set()).size + (getEventProducers(this) || new Set()).size;
  379. continue;
  380. }
  381. if (typeof eventName !== 'undefined') {
  382. assertEventName(eventName);
  383. }
  384. count += anyMap.get(this).size;
  385. for (const value of eventsMap.get(this).values()) {
  386. count += value.size;
  387. }
  388. for (const value of producersMap.get(this).values()) {
  389. count += value.size;
  390. }
  391. }
  392. return count;
  393. }
  394. bindMethods(target, methodNames) {
  395. if (typeof target !== 'object' || target === null) {
  396. throw new TypeError('`target` must be an object');
  397. }
  398. methodNames = defaultMethodNamesOrAssert(methodNames);
  399. for (const methodName of methodNames) {
  400. if (target[methodName] !== undefined) {
  401. throw new Error(`The property \`${methodName}\` already exists on \`target\``);
  402. }
  403. Object.defineProperty(target, methodName, {
  404. enumerable: false,
  405. value: this[methodName].bind(this)
  406. });
  407. }
  408. }
  409. }
  410. const allEmitteryMethods = Object.getOwnPropertyNames(Emittery.prototype).filter(v => v !== 'constructor');
  411. Object.defineProperty(Emittery, 'listenerAdded', {
  412. value: listenerAdded,
  413. writable: false,
  414. enumerable: true,
  415. configurable: false
  416. });
  417. Object.defineProperty(Emittery, 'listenerRemoved', {
  418. value: listenerRemoved,
  419. writable: false,
  420. enumerable: true,
  421. configurable: false
  422. });
  423. module.exports = Emittery;