@lumjs/core 1.36.0 → 1.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
 
3
- const {F,isObj} = require('../types');
3
+ const {F,isObj,def} = require('../types');
4
4
  const Event = require('./event');
5
5
 
6
6
  const REMOVE_OPTS = ['listener','handler','eventNames'];
@@ -127,6 +127,10 @@ class LumEventListener
127
127
  return event;
128
128
  }
129
129
 
130
+ static get classProps()
131
+ {
132
+ return Object.getOwnPropertyNames(this.prototype);
133
+ }
130
134
  }
131
135
 
132
136
  LumEventListener.isListener = isListener;
@@ -3,6 +3,7 @@
3
3
  const {S,F,isObj,def,isIterable} = require('../types');
4
4
  const Listener = require('./listener');
5
5
  const RegSym = Symbol('@lumjs/core/events:registry');
6
+ const cp = Object.assign;
6
7
 
7
8
  const DEF_EXTENDS =
8
9
  {
@@ -13,6 +14,13 @@ const DEF_EXTENDS =
13
14
  once: null,
14
15
  }
15
16
 
17
+ const INT_EXTENDS =
18
+ {
19
+ listeners: true,
20
+ results: true,
21
+ onDemand: false,
22
+ }
23
+
16
24
  const DEF_OPTIONS =
17
25
  {
18
26
  delimiter: /\s+/,
@@ -20,6 +28,12 @@ const DEF_OPTIONS =
20
28
  wildcard: '*',
21
29
  }
22
30
 
31
+ const RES_PROPS =
32
+ [
33
+ 'eventNames', 'targets', 'multiMatch', 'onceRemoved', 'stopEmitting',
34
+ 'emitted', 'targetListeners', 'registry',
35
+ ]
36
+
23
37
  /**
24
38
  * Has a target object been registered with an event registry?
25
39
  * @param {object} target
@@ -98,29 +112,53 @@ class LumEventRegistry
98
112
  * If you want to use a `function` as an actual target, you'll need to
99
113
  * wrap it in an array or other iterable object.
100
114
  *
101
- * @param {object} [opts] Options (saved to `options` property).
115
+ * @param {object} [opts] Options
116
+ *
117
+ * A _compiled_ version is saved to the `options` property.
118
+ * The compiled version includes a bunch of defaults, and various
119
+ * compose rules (mostly for the `.extend` nested options).
102
120
  *
103
121
  * @param {(RegExp|string)} [opts.delimiter=/\s+/] Used to split event names
104
122
  *
105
- * @param {(object|boolean)} [opts.extend]
106
- * This option determines the rules for adding wrapper methods and
107
- * other extension properties to the target objects.
123
+ * @param {object} [opts.extend] Options for wrapper methods/properties
124
+ *
125
+ * The `boolean` options determine if extension methods will be added to
126
+ * certain types of objects (and in some cases, when to do so).
108
127
  *
109
- * If this is `true` (default when `targets` is an `object`), then
110
- * the target objects will be extended using the default property names.
128
+ * The `?string` options are each the names of properties/methods in the
129
+ * Registry class. If they are set to a `string` then that will be the
130
+ * name used for the wrapper property/method added to objects. If it
131
+ * is explicitly set to `null` it means skip adding a wrapper for that
132
+ * method/property. If it is omitted entirely the default will be used.
111
133
  *
112
- * If this is set to `false` (default when `targets` is a `function`),
113
- * it disables adding extension properties entirely.
134
+ * @param {boolean} [opts.extend.targets] Extend target objects?
114
135
  *
115
- * If it is an `object` then each nested property may be set to a string
116
- * to override the default, or `null` to skip adding that property.
136
+ * The default will be `true` when `targets` is an `object`, or `false`
137
+ * when `targets` is a `function`.
138
+ *
139
+ * @param {boolean} [opts.extend.listeners=true] Extend Listener instances?
140
+ * As returned by `makeListener()`, `listen()`, and `once()`
141
+ * @param {boolean} [opts.extend.results=true] Extend `emit()` results?
142
+ * @param {boolean} [opts.extend.onDemand=false] On-demand target setup
143
+ *
144
+ * If `targets` was a `function` and this is set to `true`, then
145
+ * we'll perform the target setup on every emit() call. The setup process
146
+ * is skipped on any targets that have already been set up, so this
147
+ * is meant for dynamic targets that may change on every call.
148
+ *
149
+ * @param {?string} [opts.extend.registry="events"] Registry property;
150
+ * only added to `targets`, never to listeners or results which have
151
+ * their own inherent `registry` property already.
117
152
  *
118
- * @param {?string} [opts.extend.registry="events"] Registry property
119
153
  * @param {?string} [opts.extend.emit="emit"] `emit()` proxy method
120
154
  * @param {?string} [opts.extend.listen="on"] `listen()` proxy method
121
155
  * @param {?string} [opts.extend.once=null] `once()` proxy method
122
156
  * @param {?string} [opts.extend.remove=null] `remove()` proxy method
123
157
  *
158
+ * The `remove` wrapper method added to Listener instances is slightly
159
+ * different than the one added to other objects, as if you call it
160
+ * with no arguments, it will pass the Listener itself as the argument.
161
+ *
124
162
  * @param {boolean} [opts.multiMatch=false]
125
163
  * If a registered listener has multiple event names, and a call
126
164
  * to `emit()` also has multiple event names, the value of this
@@ -159,7 +197,7 @@ class LumEventRegistry
159
197
  */
160
198
  constructor(targets, opts={})
161
199
  {
162
- let defExt; // Default opts.extend value
200
+ let defExt; // Default opts.extend.targets value
163
201
  if (typeof targets === F)
164
202
  { // A dynamic getter method
165
203
  this.funTargets = true;
@@ -181,47 +219,53 @@ class LumEventRegistry
181
219
  defExt = true;
182
220
  }
183
221
 
184
- this.options = Object.assign({extend: defExt}, DEF_OPTIONS, opts);
222
+ // Build composite extend rules
223
+ const extend = cp(
224
+ {targets: defExt},
225
+ INT_EXTENDS,
226
+ DEF_EXTENDS,
227
+ opts.extend);
228
+
229
+ // Now compile the final options
230
+ this.options = cp({}, DEF_OPTIONS, opts, {extend});
185
231
 
186
232
  this.allListeners = new Set();
187
233
  this.listenersFor = new Map();
188
234
 
189
- this.extend(targets);
235
+ this.setupTargets(targets);
190
236
  } // constructor()
191
237
 
192
238
  /**
193
- * Add extension methods to target objects;
194
- * used by `constructor` and `register()`,
195
- * not meant to be called from outside code.
239
+ * Set up target objects
240
+ *
241
+ * Always sets the necessary metadata on each target.
242
+ * May also extend the targets with wrapper properties and methods
243
+ * depending on the `options.extend` values set.
244
+ *
245
+ * Not meant to be called from outside code.
196
246
  * @private
197
247
  * @param {Iterable} targets - Targets to extend
198
248
  * @returns {module:@lumjs/core/events.Registry} `this`
199
249
  */
200
- extend(targets)
250
+ setupTargets(targets)
201
251
  {
202
252
  const opts = this.options;
203
253
  const extOpts = opts.extend;
204
-
205
- let intNames = null, extNames = null;
206
-
207
- if (extOpts)
208
- {
209
- intNames = Object.keys(DEF_EXTENDS);
210
- extNames = Object.assign({}, DEF_EXTENDS, extOpts);
211
- }
254
+ const intNames = extOpts.targets ? Object.keys(DEF_EXTENDS) : null;
212
255
 
213
256
  for (const target of targets)
214
257
  {
215
258
  const tps = {}, tpm = getMetadata(target, true);
259
+ if (tpm.r.has(this)) continue; // Already set up with this registry.
216
260
  tpm.r.set(this, tps);
217
261
 
218
- if (extOpts)
262
+ if (extOpts.targets)
219
263
  {
220
264
  for (const iname of intNames)
221
265
  {
222
- if (typeof extNames[iname] === S && extNames[iname].trim() !== '')
266
+ if (typeof extOpts[iname] === S && extOpts[iname].trim() !== '')
223
267
  {
224
- const ename = extNames[iname];
268
+ const ename = extOpts[iname];
225
269
  const value = iname === 'registry'
226
270
  ? this // The registry instance itself
227
271
  : (...args) => this[iname](...args) // A proxy method
@@ -240,6 +284,48 @@ class LumEventRegistry
240
284
  }
241
285
  }
242
286
  }
287
+
288
+ return this;
289
+ }
290
+
291
+ /**
292
+ * Add extension methods to internal objects;
293
+ * currently supports Listener instances and result/status objects.
294
+ * Not meant to be called from outside code.
295
+ * @private
296
+ * @param {object} obj - Internal object to extend
297
+ * @returns {module:@lumjs/core/events.Registry} `this`
298
+ */
299
+ extendInternal(obj)
300
+ {
301
+ const opts = this.options;
302
+ const extOpts = opts.extend;
303
+ const intNames = Object.keys(DEF_EXTENDS);
304
+ const isLs = (obj instanceof Listener);
305
+ const reserved = isLs ? Listener.classProps : RES_PROPS;
306
+
307
+ for (const iname of intNames)
308
+ {
309
+ if (iname === 'registry') continue; // skip the registry property
310
+ if (typeof extOpts[iname] === S && extOpts[iname].trim() !== '')
311
+ {
312
+ const ename = extOpts[iname];
313
+ if (reserved.includes(ename))
314
+ { // Skip reserved names
315
+ console.warn("reserved property", {ename, obj, registry: this});
316
+ continue;
317
+ }
318
+
319
+ const value = (isLs && iname === 'remove')
320
+ ? (what=obj) => this.remove(what) // special remove wrapper
321
+ : (...args) => this[iname](...args) // regular wrapper method
322
+ if (opts.overwrite || obj[ename] === undefined)
323
+ {
324
+ def(obj, ename, {value});
325
+ }
326
+ }
327
+ }
328
+
243
329
  return this;
244
330
  }
245
331
 
@@ -305,16 +391,21 @@ class LumEventRegistry
305
391
  }
306
392
  else if (args.length === 1 && isObj(args[0]))
307
393
  { // listen(spec)
308
- spec = Object.assign({}, args[0]);
394
+ spec = cp({}, args[0]);
309
395
  }
310
396
  else
311
397
  { // listen(eventNames, listener, [spec])
312
- spec = Object.assign({}, args[2]);
398
+ spec = cp({}, args[2]);
313
399
  spec.eventNames = args[0];
314
400
  spec.handler = args[1];
315
401
  }
316
402
 
317
- return new Listener(this, spec);
403
+ const lsnr = new Listener(this, spec);
404
+ if (this.options.extend.listeners)
405
+ {
406
+ this.extendInternal(lsnr);
407
+ }
408
+ return lsnr;
318
409
  }
319
410
 
320
411
  /**
@@ -524,6 +615,7 @@ class LumEventRegistry
524
615
  */
525
616
  emit(eventNames, ...args)
526
617
  {
618
+ const extOpts = this.options.extend;
527
619
  const sti =
528
620
  {
529
621
  eventNames: this.getEventNames(eventNames),
@@ -531,11 +623,21 @@ class LumEventRegistry
531
623
  onceRemoved: new Set(),
532
624
  stopEmitting: false,
533
625
  emitted: [],
626
+ registry: this,
627
+ }
628
+
629
+ if (extOpts.results)
630
+ {
631
+ this.extendInternal(sti);
534
632
  }
535
633
 
536
634
  { // Get the targets.
537
635
  const tgs = this.getTargets(sti);
538
636
  sti.targets = (tgs instanceof Set) ? tgs : new Set(tgs);
637
+ if (this.funTargets && extOpts.onDemand)
638
+ {
639
+ this.setupTargets(sti.targets);
640
+ }
539
641
  }
540
642
 
541
643
  const wilds = this.listenersFor.get(this.options.wildcard);
@@ -579,8 +681,6 @@ class LumEventRegistry
579
681
  return sti;
580
682
  }
581
683
 
582
-
583
-
584
684
  /**
585
685
  * Register additional target objects
586
686
  * @param {...object} addTargets - Target objects to register
@@ -617,7 +717,7 @@ class LumEventRegistry
617
717
  }
618
718
  }
619
719
 
620
- this.extend(addTargets);
720
+ this.setupTargets(addTargets);
621
721
  return this;
622
722
  }
623
723
 
@@ -735,7 +835,7 @@ class LumEventRegistry
735
835
 
736
836
  }
737
837
 
738
- Object.assign(LumEventRegistry,
838
+ cp(LumEventRegistry,
739
839
  {
740
840
  isRegistered, getMetadata, targetsAre,
741
841
  });
package/lib/obj/clone.js CHANGED
@@ -296,6 +296,9 @@ exports.addClone = addClone;
296
296
  *
297
297
  * If not, if the object has a `clone()` method it will be used.
298
298
  * Otherwise use our `clone()` function.
299
+ *
300
+ * **NOTE**: A newer replacement for this function exists, see
301
+ * {@link module:@lumjs/core/obj.unlocked} for details.
299
302
  *
300
303
  * @param {object} obj - The object to clone if needed.
301
304
  * @param {object} [opts] - Options to pass to `clone()` method.
package/lib/obj/index.js CHANGED
@@ -14,6 +14,7 @@ const {lock,addLock} = require('./lock');
14
14
  const {mergeNested,syncNested} = require('./merge');
15
15
  const ns = require('./ns');
16
16
  const cp = require('./cp');
17
+ const unlocked = require('./unlocked');
17
18
 
18
19
  const
19
20
  {
@@ -27,5 +28,5 @@ module.exports =
27
28
  mergeNested, syncNested, copyProps, copyAll, ns,
28
29
  getObjectPath, setObjectPath, getNamespace, setNamespace,
29
30
  getProperty, duplicateAll, duplicateOne, getMethods, signatureOf,
30
- MethodFilter, apply, flip, flipKeyVal, flipMap,
31
+ MethodFilter, apply, flip, flipKeyVal, flipMap, unlocked,
31
32
  }
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+
3
+ const {F,S,isObj,needObj} = require('../types');
4
+
5
+ const CLONE = 'clone';
6
+ const clone = obj => Object.assign({}, obj);
7
+
8
+ /**
9
+ * Get an unlocked object
10
+ *
11
+ * @param {object} obj - The target object;
12
+ *
13
+ * If the object is extensible it will be returned _as is_.
14
+ *
15
+ * If the object is frozen, sealed, or otherwise non-extensible,
16
+ * a cloning function will be used to make an unlocked copy.
17
+ *
18
+ * @param {(object|function|string)} [opts] Options to customize the behaviour
19
+ *
20
+ * - If this is a `function` it will be used as the `opts.fn` value.
21
+ * - If this is a `string` it will be used as the `opts.method` value.
22
+ *
23
+ * @param {string} [opts.method='clone'] Object method to use
24
+ *
25
+ * Set to an empty string `""` to skip checking for an object method.
26
+ *
27
+ * If the method exists on the target object, it will be called to
28
+ * perform the cloning procedure, otherwise `opts.fn` will be used.
29
+ *
30
+ * @param {Array} [opts.args] Arguments for `opts.method` method
31
+ *
32
+ * If not specified, the default value is: `[opts]`
33
+ *
34
+ * @param {function} [opts.fn] A function to perform the clone operation
35
+ *
36
+ * Must take only a single parameter, which is the object to clone.
37
+ * Must return an extensible clone of the object.
38
+ *
39
+ * If for whatever reason you need the `opts` in the function,
40
+ * then use a unbound function, and `opts` will be available
41
+ * as `this` in the function body. Arrow functions (closures) are
42
+ * always considered bound and cannot have a `this` value assigned.
43
+ *
44
+ * The default value is a closure: `obj => Object.assign({}, obj)`;
45
+ *
46
+ * @return {object}
47
+ *
48
+ * @alias module:@lumjs/core/obj.unlocked
49
+ */
50
+ function unlocked(obj, opts={})
51
+ {
52
+ needObj(obj, true, "invalid obj value");
53
+
54
+ if (Object.isExtensible(obj))
55
+ { // We're done here.
56
+ return obj;
57
+ }
58
+
59
+ if (typeof opts === F)
60
+ {
61
+ opts = {fn: opts}
62
+ }
63
+ else if (typeof opts === S)
64
+ {
65
+ opts = {method: opts}
66
+ }
67
+ else if (!isObj(opts))
68
+ {
69
+ console.error({obj, opts});
70
+ throw new TypeError("invalid opts value");
71
+ }
72
+
73
+ const fn = (typeof opts.fn === F) ? opts.fn : clone;
74
+ const meth = (typeof opts.method === S) ? opts.method.trim() : CLONE;
75
+ const args = (Array.isArray(opts.args)) ? opts.args : [opts];
76
+
77
+ if (meth && typeof obj[meth] === F)
78
+ { // Use a clone method
79
+ return obj[meth](...args);
80
+ }
81
+ else
82
+ { // Use a clone function
83
+ return fn.call(opts, obj);
84
+ }
85
+ }
86
+
87
+ module.exports = unlocked;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumjs/core",
3
- "version": "1.36.0",
3
+ "version": "1.37.0",
4
4
  "main": "lib/index.js",
5
5
  "exports":
6
6
  {