-
-
Notifications
You must be signed in to change notification settings - Fork 205
Expand file tree
/
Copy pathNodeEventDispatcher.js
More file actions
410 lines (369 loc) · 17.5 KB
/
Copy pathNodeEventDispatcher.js
File metadata and controls
410 lines (369 loc) · 17.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
/*
* Copyright (c) 2021 - present core.ai . All rights reserved.
* Original work Copyright (c) 2014 - 2021 Adobe Systems Incorporated. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
// This is the exact same code as `src/utils/EventDispatcher.js` to load in node so that the same API in
// Phoenix browser for event dispatcher will work in node too.
/**
* Implements a jQuery-like event dispatch pattern for non-DOM objects (works in web workers and phoenix node as well):
* - Listeners are attached via on()/one() & detached via off()
* - Listeners can use namespaces for easy removal
* - Listeners can attach to multiple events at once via a space-separated list
* - Events are fired via trigger()
* - The same listener can be attached twice, and will be called twice; but off() will detach all
* duplicate copies at once ('duplicate' means '===' equality - see http://jsfiddle.net/bf4p29g5/1/)
*
* But it has some important differences from jQuery's non-DOM event mechanism:
* - More robust to listeners that throw exceptions (other listeners will still be called, and
* trigger() will still return control to its caller).
* - Events can be marked deprecated, causing on() to issue warnings
* - Easier to debug, since the dispatch code is much simpler
* - Faster, for the same reason
* - Uses less memory, since $(nonDOMObj).on() leaks memory in jQuery
* - API is simplified:
* - Event handlers do not have 'this' set to the event dispatcher object
* - Event object passed to handlers only has 'type' and 'target' fields
* - trigger() uses a simpler argument-list signature (like Promise APIs), rather than requiring
* an Array arg and ignoring additional args
* - trigger() does not support namespaces
* - For simplicity, on() does not accept a map of multiple events -> multiple handlers, nor a
* missing arg standing in for a bare 'return false' handler.
*
* For now, Brackets uses a jQuery patch to ensure $(obj).on() and obj.on() (etc.) are identical
* for any obj that has the EventDispatcher pattern. In the future, this may be deprecated.
*
* To add EventDispatcher methods to any object, call EventDispatcher.makeEventDispatcher(obj).
*
* ## Usage
* ### Importing from an extension
* ```js
* const EventDispatcher = brackets.getModule("utils/EventDispatcher");
* ```
* ### Using the global object
* The EventDispatcher Object is available within the global context, be it phoenix or phoenix core web workers or node.
* ```js
* window.EventDispatcher.makeEventDispatcher(exports); // within phoenix require module
* self.EventDispatcher.makeEventDispatcher(object); // within web worker
* global.EventDispatcher.makeEventDispatcher(exports); // within node module that has an export
* ```
*
* If you wish to import event dispatcher to your custom web worker, use the following
* ```js
* importScripts('<relative path from your extension>/utils/EventDispatcher');
* // this will add the global EventDispatcher to your web-worker. Note that the EventDispatcher in the web worker
* // and node is a separate domain and cannot raise or listen to events in phoenix/other workers. For triggering events
* // between different domains like between node and phcode, see `nodeConnector.triggerPeer` or
* // `WorkerComm.triggerPeer` API for communication between phcode and web workers.
* self.EventDispatcher.trigger("someEvent"); // within web worker
* ```
* ### Sample Usage within extension
* ```js
* // in your extension js file.
* define (function (require, exports, module) {
* const EventDispatcher = brackets.getModule("utils/EventDispatcher");
* EventDispatcher.makeEventDispatcher(exports); // This extension triggers some events
* let eventHandler = function (event, paramObject, paramVal) {
* console.log(event, paramObject, paramVal);
* };
* exports.on("sampleEvent", eventHandler); // listen to our own event for demo
* exports.trigger("sampleEvent", { // trigger a sample event. This will activate the above listener 'on' function.
* param: 1,
* param2: "sample"
* }, "value");
* // If needed, the event listener can be removed with `off`. But it is not a requirement at shutdown.
* exports.off("sampleEvent", eventHandler);
* }
* ```
*
* @module utils/EventDispatcher
*/
(function () {
let globalObject = {};
if(typeof window !== 'undefined'){
globalObject = window; // browser
} else if(typeof self !== 'undefined'){
globalObject = self; // web worker
} else if(typeof global !== 'undefined'){
globalObject = global; //nodejs
}
if(globalObject.EventDispatcher){
// already created
return;
}
let LEAK_WARNING_THRESHOLD = 15,
eventSpecificThreshold = {};
/**
* Split "event.namespace" string into its two parts; both parts are optional.
* @param {string} eventName Event name and/or trailing ".namespace"
* @return {!{event:string, ns:string}} Uses "" for missing parts.
* @type {function}
*/
function splitNs(eventStr) {
var dot = eventStr.indexOf(".");
if (dot === -1) {
return { eventName: eventStr };
}
return { eventName: eventStr.substring(0, dot), ns: eventStr.substring(dot) };
}
/**
* By default, we consider any events having more than 15 listeners to be leaky. But sometimes there may be
* genuine use cases where an event can have a large number of listeners. For those events, it is recommended
* to increase the leaky warning threshold individually with this API.
* @param {string} eventName
* @param {number} threshold - The new threshold to set. Will only be set if the new threshold is greater than
* the current threshold.
* @type {function}
*/
function setLeakThresholdForEvent(eventName, threshold) {
let currentThreshold = eventSpecificThreshold[eventName] || LEAK_WARNING_THRESHOLD;
if(threshold > currentThreshold){
eventSpecificThreshold[eventName] = threshold;
}
}
// These functions are added as mixins to any object by makeEventDispatcher()
/**
* Adds the given handler function to 'events': a space-separated list of one or more event names, each
* with an optional ".namespace" (used by off() - see below). If the handler is already listening to this
* event, a duplicate copy is added.
* @param {string} events
* @param {!function(!{type:string, target:!Object}, ...)} fn
* @type {function}
*/
var on = function (events, fn) {
var eventsList = events.split(/\s+/).map(splitNs),
i;
if (!fn) {
throw new Error("EventListener.on() called with no listener fn for event '" + events + "'");
}
// Check for deprecation warnings
if (this._deprecatedEvents) {
for (i = 0; i < eventsList.length; i++) {
var deprecation = this._deprecatedEvents[eventsList[i].eventName];
if (deprecation) {
var message = "Registering for deprecated event '" + eventsList[i].eventName + "'.";
if (typeof deprecation === "string") {
message += " Instead, use " + deprecation + ".";
}
console.warn(message, new Error().stack);
}
}
}
// Attach listener for each event clause
for (i = 0; i < eventsList.length; i++) {
var eventName = eventsList[i].eventName;
if (!this._eventHandlers) {
this._eventHandlers = {};
}
if (!this._eventHandlers[eventName]) {
this._eventHandlers[eventName] = [];
}
eventsList[i].handler = fn;
this._eventHandlers[eventName].push(eventsList[i]);
// Check for suspicious number of listeners being added to one object-event pair
let leakThreshold = eventSpecificThreshold[eventName] || LEAK_WARNING_THRESHOLD;
if (this._eventHandlers[eventName].length > leakThreshold) {
console.error("Possible memory leak: " +
this._eventHandlers[eventName].length + " '" + eventName + "' listeners attached to", this);
}
}
return this; // for chaining
};
/**
* Removes one or more handler functions based on the space-separated 'events' list. Each item in
* 'events' can be: bare event name, bare .namespace, or event.namespace pair. This yields a set of
* matching handlers. If 'fn' is omitted, all these handlers are removed. If 'fn' is provided,
* only handlers exactly equal to 'fn' are removed (there may still be >1, if duplicates were added).
* @param {string} events
* @param {?function(!{type:string, target:!Object}, ...)} fn
* @type {function}
*/
var off = function (events, fn) {
if (!this._eventHandlers) {
return this;
}
var eventsList = events.split(/\s+/).map(splitNs),
i;
var removeAllMatches = function (eventRec, eventName) {
var handlerList = this._eventHandlers[eventName],
k;
if (!handlerList) {
return;
}
// Walk backwards so it's easy to remove items
for (k = handlerList.length - 1; k >= 0; k--) {
// Look at ns & fn only - doRemove() has already taken care of eventName
if (!eventRec.ns || eventRec.ns === handlerList[k].ns) {
var handler = handlerList[k].handler;
if (!fn || fn === handler || fn._eventOnceWrapper === handler) {
handlerList.splice(k, 1);
}
}
}
if (!handlerList.length) {
delete this._eventHandlers[eventName];
}
}.bind(this);
var doRemove = function (eventRec) {
if (eventRec.eventName) {
// If arg calls out an event name, look at that handler list only
removeAllMatches(eventRec, eventRec.eventName);
} else {
// If arg only gives a namespace, look at handler lists for all events
for (let eventName in this._eventHandlers) {
removeAllMatches(eventRec, eventName);
}
}
}.bind(this);
// Detach listener for each event clause
// Each clause may be: bare eventname, bare .namespace, full eventname.namespace
for (i = 0; i < eventsList.length; i++) {
doRemove(eventsList[i]);
}
return this; // for chaining
};
/**
* Attaches a handler so it's only called once (per event in the 'events' list).
* @param {string} events
* @param {?function(!{type:string, target:!Object}, ...)} fn
* @type {function}
*/
var one = function (events, fn) {
// Wrap fn in a self-detaching handler; saved on the original fn so off() can detect it later
if (!fn._eventOnceWrapper) {
fn._eventOnceWrapper = function (event) {
// Note: this wrapper is reused for all attachments of the same fn, so it shouldn't reference
// anything from the outer closure other than 'fn'
event.target.off(event.type, fn._eventOnceWrapper);
fn.apply(this, arguments);
};
}
return this.on(events, fn._eventOnceWrapper);
};
/**
* Invokes all handlers for the given event (in the order they were added).
* @param {string} eventName
* @param {*} ... Any additional args are passed to the event handler after the event object
* @type {function}
*/
var trigger = function (eventName) {
var event = { type: eventName, target: this },
handlerList = this._eventHandlers && this._eventHandlers[eventName],
i;
if (!handlerList) {
return;
}
// Use a clone of the list in case handlers call on()/off() while we're still in the loop
handlerList = handlerList.slice();
// Pass 'event' object followed by any additional args trigger() was given
var applyArgs = Array.prototype.slice.call(arguments, 1);
applyArgs.unshift(event);
for (i = 0; i < handlerList.length; i++) {
try {
// Call one handler
handlerList[i].handler.apply(null, applyArgs);
} catch (err) {
console.error("Exception in '" + eventName + "' listener on", this, String(err), err.stack);
}
}
};
/**
* Adds the EventDispatcher APIs to the given object: on(), one(), off(), and trigger(). May also be
* called on a prototype object - each instance will still behave independently.
* @param {!Object} obj Object to add event-dispatch methods to
* @type {function}
*/
function makeEventDispatcher(obj) {
obj.on = on;
obj.off= off;
obj.one= one;
obj.trigger= trigger;
obj._EventDispatcher= true;
// Later, on() may add _eventHandlers: Object.<string, Array.<{event:string, namespace:?string,
// handler:!function(!{type:string, target:!Object}, ...)}>> - map from eventName to an array
// of handler records
// Later, markDeprecated() may add _deprecatedEvents: Object.<string, string|boolean> - map from
// eventName to deprecation warning info
}
/**
* Utility for calling on() with an array of arguments to pass to event handlers (rather than a varargs
* list). makeEventDispatcher() must have previously been called on 'dispatcher'.
* @param {!Object} dispatcher
* @param {string} eventName
* @param {!Array.<*>} argsArray
* @type {function}
*/
function triggerWithArray(dispatcher, eventName, argsArray) {
var triggerArgs = [eventName].concat(argsArray);
dispatcher.trigger.apply(dispatcher, triggerArgs);
}
/**
* Utility for attaching an event handler to an object that has not YET had makeEventDispatcher() called
* on it, but will in the future. Once 'futureDispatcher' becomes a real event dispatcher, any handlers
* attached here will be retained.
*
* Useful with core modules that have circular dependencies (one module initially gets an empty copy of the
* other, with no on() API present yet). Unlike other strategies like waiting for htmlReady(), this helper
* guarantees you won't miss any future events, regardless of how soon the other module finishes init and
* starts calling trigger().
*
* @param {!Object} futureDispatcher
* @param {string} events
* @param {?function(!{type:string, target:!Object}, ...)} fn
* @type {function}
*/
function on_duringInit(futureDispatcher, events, fn) {
on.call(futureDispatcher, events, fn);
}
/**
* Mark a given event name as deprecated, such that on() will emit warnings when called with it.
* May be called before makeEventDispatcher(). May be called on a prototype where makeEventDispatcher()
* is called separately per instance (i.e. in the constructor). Should be called before clients have
* a chance to start calling on().
* @param {!Object} obj Event dispatcher object
* @param {string} eventName Name of deprecated event
* @param {string=} insteadStr Suggested thing to use instead
* @type {function}
*/
function markDeprecated(obj, eventName, insteadStr) {
// Mark event as deprecated - on() will emit warnings when called with this event
if (!obj._deprecatedEvents) {
obj._deprecatedEvents = {};
}
obj._deprecatedEvents[eventName] = insteadStr || true;
}
globalObject.EventDispatcher = {
makeEventDispatcher,
triggerWithArray,
on_duringInit,
markDeprecated,
setLeakThresholdForEvent
};
if(globalObject.define){
// for requirejs support
define(function (require, exports, module) {
exports.makeEventDispatcher = globalObject.EventDispatcher.makeEventDispatcher;
exports.triggerWithArray = globalObject.EventDispatcher.triggerWithArray;
exports.on_duringInit = globalObject.EventDispatcher.on_duringInit;
exports.markDeprecated = globalObject.EventDispatcher.markDeprecated;
exports.setLeakThresholdForEvent = globalObject.EventDispatcher.setLeakThresholdForEvent;
});
}
}());