@lumjs/core 1.35.1 → 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;
@@ -1,18 +1,11 @@
1
- /**
2
- * A very simplistic Trait system.
3
- * @module @lumjs/core/traits
4
- */
5
-
6
1
  "use strict";
7
2
 
8
- const getProp = require('./obj/getproperty');
9
-
3
+ const getProp = require('../obj/getproperty');
10
4
  const
11
5
  {
12
- def,F,B,S,
6
+ def,F,B,needObj,
13
7
  isObj,isArray,isConstructor,isProperty,isClassObject,
14
- needObj,needType,
15
- } = require('./types');
8
+ } = require('../types');
16
9
 
17
10
  // Symbol for private storage of composed traits.
18
11
  const COMPOSED_TRAITS = Symbol.for('@lumjs/core/traits~ComposedTraits');
@@ -568,254 +561,10 @@ function decompose(specTarget, specSource, opts={})
568
561
  return c;
569
562
  }
570
563
 
571
- /**
572
- * An abstract class for Traits.
573
- *
574
- * Simply offers a couple static methods and APIs that
575
- * wrap the `compose()` and `composeFully()` functions,
576
- * and makes it fairly simple to create Trait classes.
577
- *
578
- * @alias module:@lumjs/core/traits.Trait
579
- */
580
- class CoreTrait
581
- {
582
- /**
583
- * Extend another class or object instance with the methods and
584
- * getter/setter properties from a Trait.
585
- *
586
- * The sub-class of Trait this static method is called on will always
587
- * be the `source` argument.
588
- *
589
- * @param {(function|object)} target - Target class or instance.
590
- *
591
- * @param {object} [protoOpts] Options for `compose()` function.
592
- *
593
- * If this is not specified, or is any value other than an `object`,
594
- * we will look for defaults in a `composeOptions` static property:
595
- *
596
- * ```js
597
- * static get composeOptions() { return {your: default, options: here}; }
598
- * ```
599
- *
600
- * @param {(object|true)} [staticOpts] Static options.
601
- *
602
- * If this is set we'll use `composeFully()` instead of using `compose()`.
603
- *
604
- * If this value is an `object` it will be used as the `staticOpts`.
605
- *
606
- * If this is the special value `true`, then we will look for the options
607
- * in a `staticOptions` static property:
608
- *
609
- * ```js
610
- * static get staticOptions() { return {your: static, options: here}; }
611
- * ```
612
- *
613
- * If this any value other than an `object` or `true`, it will be ignored
614
- * entirely, and the regular `compose()` call will be used.
615
- *
616
- * @returns {object} Return value from the `setupTrait()` static method.
617
- *
618
- */
619
- static composeInto(target, protoOpts, staticOpts)
620
- {
621
- if (!isObj(protoOpts))
622
- {
623
- protoOpts = this.composeOptions ?? {};
624
- }
625
-
626
- if (staticOpts === true)
627
- {
628
- staticOpts = this.staticOptions ?? {};
629
- }
630
-
631
- let composed;
632
-
633
- if (isObj(staticOpts))
634
- {
635
- composed = composeFully(target, this, protoOpts, staticOpts);
636
- }
637
- else
638
- {
639
- composed = compose(target, this, protoOpts);
640
- }
641
-
642
- return this.setupTrait({target, protoOpts, staticOpts, composed});
643
- }
644
-
645
- /**
646
- * A static method called by `composeInto()`
647
- * _after_ composing the trait properties into the target.
648
- *
649
- * @param {object} info - Metadata from `composeInto()`
650
- * @param {(function|object)} info.target - The `target` argument
651
- * @param {object} info.protoOpts - The `protoOpts` used
652
- * @param {object} [info.staticOpts] The `staticOpts` if used
653
- * @param {module:@lumjs/core/traits~Composed} info.composed
654
- * The return value from `compose()` or `composeFully()`.
655
- *
656
- * @returns {object} The `info` object, with any changes made
657
- * by an overridden `setupTrait()` method in the sub-class.
658
- *
659
- * The default implementation is a placeholder that returns the
660
- * `info` object without making any changes.
661
- *
662
- */
663
- static setupTrait(info)
664
- {
665
- if (this.debug)
666
- {
667
- console.debug(this.name, "setupTrait()", info, this);
668
- }
669
- return info;
670
- }
671
-
672
- /**
673
- * A method wrapping {@link module:@lumjs/core/traits.decompose}
674
- * where the `source` is always the Trait sub-class constructor.
675
- *
676
- * See the `decompose()` docs for descriptions of the other arguments.
677
- *
678
- * @param {(function|object)} target
679
- * @param {object} [opts]
680
- * @returns {object} Return value from the `removedTrait()` static method.
681
- */
682
- static decomposeFrom(target, opts)
683
- {
684
- const info = {target, ok:true};
685
- info.composed = this.getComposed(target);
686
- this.removeTrait(info);
687
-
688
- if (info.ok)
689
- {
690
- info.count = decompose(target, this, opts);
691
- }
692
-
693
- return this.removedTrait(info);
694
- }
695
-
696
- /**
697
- * A static method called by `decomposeFrom()`
698
- * _before_ decomposing the trait properties from the target.
699
- *
700
- * @param {object} info - Metadata from `decomposeFrom()`
701
- * @param {(function|object)} info.target - The `target` argument
702
- * @param {module:@lumjs/core/traits~Composed} info.composed
703
- * The property map that was previously composed.
704
- * @param {boolean} info.ok - Will always be `true` initially.
705
- *
706
- * If an overridden `removeTrait()` method sets this to `false`,
707
- * then the decomposeFrom() operation will skip the step of
708
- * actually decomposing the trait.
709
- *
710
- * @returns {*} Return value is not used.
711
- */
712
- static removeTrait(info)
713
- {
714
- if (this.debug)
715
- {
716
- console.debug(this.name, "removeTrait()", info, this);
717
- }
718
- return info;
719
- }
720
-
721
- /**
722
- * A static method called by `decomposeFrom()`
723
- * _after_ decomposing the trait properties from the target.
724
- *
725
- * @param {object} info - The same as `removeTrait()`, plus:
726
- * @param {number} info.count - The number of properties decomposed;
727
- *
728
- * @returns {object} The `info` object, with any changes made
729
- * by `removeTrait()` and `removedTrait()` methods in the
730
- * sub-class.
731
- *
732
- * The default implementation is a placeholder that returns the
733
- * `info` object without making any changes.
734
- *
735
- */
736
- static removedTrait(info)
737
- {
738
- if (this.debug)
739
- {
740
- console.debug(this.name, "removedTrait()", info, this);
741
- }
742
- return info;
743
- }
744
-
745
- /**
746
- * A method wrapping {@link module:@lumjs/core/traits.getComposed}
747
- * where the `source` is always the Trait sub-class constructor.
748
- *
749
- * @param {(function|object)} target
750
- * @returns {mixed} Return value from `getComposed()`
751
- */
752
- static getComposed(target)
753
- {
754
- return getComposed(target, this);
755
- }
756
-
757
- } // CoreTrait class
758
-
759
- /**
760
- * Build a Trait registry.
761
- *
762
- * @param {object} registry - Object for the registry.
763
- *
764
- * Generally the `exports` from a Node.js module would be good here.
765
- *
766
- * It will have a `Trait` property added, which is an alias to
767
- * the `Trait` class constructor.
768
- *
769
- * It will also have a `registerTrait(name, value)` function added.
770
- * This function will add new traits to the registry, using
771
- * the `name` as its property key. The `value` is either the
772
- * Trait sub-class constructor `function` itself, or a _lazy-loading_
773
- * closure `function` that must load and return the actual sub-class
774
- * constructor when executed. The registry DOES NOT support `object`
775
- * type traits, or any class that doesn't extend the `Trait` class.
776
- *
777
- * @returns {function} The `registerTrait()` function created above.
778
- * @alias module:@lumjs/core/traits.makeRegistry
779
- */
780
- function makeTraitRegistry(registry={})
781
- {
782
- needObj(registry, false, 'invalid trait registry object');
783
-
784
- def(registry, 'Trait', CoreTrait);
785
-
786
- function registerTrait(name, value)
787
- {
788
- needType(S, name, 'invalid trait name');
789
- needType(F, value, 'invalid trait loader value');
790
-
791
- if (registry[name] !== undefined)
792
- {
793
- console.error("trait already registered", {name,value,registry});
794
- return;
795
- }
796
-
797
- if (CoreTrait.isPrototypeOf(value))
798
- { // Make it available directly.
799
- def(registry, name, value, def.e);
800
- }
801
- else
802
- { // Lazy-loading engaged.
803
- def.lazy(registry, name, value, def.e);
804
- }
805
- }
806
-
807
- def(registry, 'registerTrait', registerTrait);
808
-
809
- return registerTrait;
810
- }
811
-
812
564
  module.exports =
813
565
  {
814
566
  compose, composeFully, getComposed, decompose,
815
- Trait: CoreTrait, IGNORE_STATIC,
816
- ensureProto, ensureConstructor,
817
- makeRegistry: makeTraitRegistry,
818
-
567
+ IGNORE_STATIC, ensureProto, ensureConstructor,
819
568
  // Undocumented:
820
569
  hasOwn,
821
570
  }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * A very simplistic Trait system.
3
+ * @module @lumjs/core/traits
4
+ */
5
+
6
+ "use strict";
7
+
8
+ const funcs = require('./funcs');
9
+ const Trait = require('./trait');
10
+ const regfns = require('./registry');
11
+
12
+ // Export all the things
13
+ Object.assign(exports, funcs, regfns, {Trait});
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+
3
+ const {def,F,S,needObj,needType} = require('../types');
4
+ const Trait = require('./trait');
5
+
6
+ /**
7
+ * Get a trait from the specified registry
8
+ *
9
+ * The exported function version of this is not generally called directly.
10
+ * Instead use the bound `getTrait()` method of a registry object.
11
+ *
12
+ * @alias module:@lumjs/core/traits.getTrait
13
+ *
14
+ * @param {module:@lumjs/core/traits~Registry} registry
15
+ * @param {(string|function)} trait - Trait to get
16
+ *
17
+ * This should almost always be the name of the trait you want
18
+ * to get out of this registry.
19
+ *
20
+ * If this happens to be a Trait class constructor already,
21
+ * it will be returned as is.
22
+ *
23
+ * @returns {?function} A trait class constructor, or `null` if the
24
+ * specified trait name does not exist in the registry.
25
+ */
26
+ function getTrait(registry,trait)
27
+ {
28
+ if (typeof trait === S)
29
+ { // Assume a string is the name of a trait in this registry
30
+ trait = registry[trait];
31
+ }
32
+
33
+ if (typeof trait === F && Trait.isPrototypeOf(trait))
34
+ {
35
+ return trait;
36
+ }
37
+ else
38
+ {
39
+ console.error("invalid trait", {trait});
40
+ return null;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Get a bunch of traits from the specified registry
46
+ *
47
+ * The exported function version of this is not generally called directly.
48
+ * Instead use the bound `getTraits()` method from a registry object.
49
+ *
50
+ * @alias module:@lumjs/core/traits.getTraits
51
+ *
52
+ * @param {module:@lumjs/core/traits~Registry} registry
53
+ * @param {Iterable<(string|function)>} inList - List of traits to get
54
+ *
55
+ * Every item from this list will be passed to `getTrait()` to get
56
+ * the Trait class constructors for the returned set.
57
+ *
58
+ * @param {Set} [outSet=new Set] Will be populated with requested traits
59
+ *
60
+ * @returns {Set} The `outSet` with all traits added to it
61
+ */
62
+ function getTraits(registry, inList, outSet)
63
+ {
64
+ if (!outSet)
65
+ {
66
+ outSet=new Set();
67
+ }
68
+ else if (!(outSet instanceof Set))
69
+ {
70
+ console.error({inList, outSet});
71
+ throw new TypeError("Invalid Set object");
72
+ }
73
+
74
+ for (const ti of inList)
75
+ {
76
+ const tc = getTrait(registry, ti);
77
+ if (tc)
78
+ {
79
+ outSet.add(tc);
80
+ }
81
+ }
82
+
83
+ return outSet;
84
+ }
85
+
86
+ /**
87
+ * Register a trait class in a registry
88
+ *
89
+ * The exported function version of this is not generally called directly.
90
+ * Instead use the bound `registerTrait()` method from a registry object.
91
+ *
92
+ * @function module:@lumjs/core/traits.registerTrait
93
+ *
94
+ * @param {module:@lumjs/core/traits~Registry} registry
95
+ * @param {string} name - Name to use for the trait in the registry
96
+ * @param {function} getTrait
97
+ *
98
+ * If the function passed is a class constructor with the `Trait`
99
+ * class in its prototype chain, it will be used as the trait itself.
100
+ *
101
+ * If it is just a regular function or closure, it will be used as
102
+ * a lazy-loader that must return the actual class constructor.
103
+ *
104
+ * @param {boolean} [overwrite=false]
105
+ *
106
+ * @returns {module:@lumjs/core/traits~Registry} The registry object
107
+ *
108
+ * As a special exception to the argument type checking, if you omit both
109
+ * arguments, then the registry object will be returned immediately.
110
+ *
111
+ * @throws {TypeError} If `name` or `getTrait` were not valid values
112
+ */
113
+
114
+ function registerTrait(registry, name, getTrait, overwrite=false)
115
+ {
116
+ if (name === undefined && getTrait === undefined)
117
+ { // A special case, return the registry object
118
+ return registry;
119
+ }
120
+
121
+ needType(S, name, 'invalid trait name');
122
+ needType(F, getTrait, 'invalid trait loader value');
123
+
124
+ if (!overwrite && registry[name] !== undefined)
125
+ {
126
+ console.error("trait already registered", {name,getTrait,registry});
127
+ return registry;
128
+ }
129
+
130
+ if (Trait.isPrototypeOf(getTrait))
131
+ { // Make it available directly.
132
+ def(registry, name, getTrait, def.e);
133
+ }
134
+ else
135
+ { // Lazy-loading engaged.
136
+ def.lazy(registry, name, getTrait, def.e);
137
+ }
138
+
139
+ return registry;
140
+ }
141
+
142
+ /**
143
+ * Build a Trait registry.
144
+ *
145
+ * @param {object} [registry={}] Object for the registry
146
+ *
147
+ * Generally the `exports` from a Node.js module would be good here.
148
+ * The object will have several properties and methods assigned to it
149
+ * see {@link module:@lumjs/core/traits~Registry} for details.
150
+ *
151
+ * @returns {function} A
152
+ * @alias module:@lumjs/core/traits.makeRegistry
153
+ */
154
+ function makeTraitRegistry(registry={})
155
+ {
156
+ needObj(registry, false, 'invalid trait registry object');
157
+
158
+ def(registry, 'Trait', Trait);
159
+ def(registry, 'getTrait', getTrait.bind(registry, registry));
160
+ def(registry, 'getTraits', getTraits.bind(registry, registry));
161
+ def(registry, 'registerTrait', registerTrait.bind(registry, registry));
162
+
163
+ return registerTrait;
164
+ }
165
+
166
+ module.exports = makeTraitRegistry;
@@ -0,0 +1,194 @@
1
+ "use strict";
2
+
3
+ const {isObj} = require('../types');
4
+ const {getComposed,decompose,compose,composeFully} = require('./funcs');
5
+
6
+ /**
7
+ * An abstract class for Traits.
8
+ *
9
+ * Simply offers a couple static methods and APIs that
10
+ * wrap the `compose()` and `composeFully()` functions,
11
+ * and makes it fairly simple to create Trait classes.
12
+ *
13
+ * @alias module:@lumjs/core/traits.Trait
14
+ */
15
+ class CoreTrait
16
+ {
17
+ /**
18
+ * Extend another class or object instance with the methods and
19
+ * getter/setter properties from a Trait.
20
+ *
21
+ * The sub-class of Trait this static method is called on will always
22
+ * be the `source` argument.
23
+ *
24
+ * @param {(function|object)} target - Target class or instance.
25
+ *
26
+ * @param {object} [protoOpts] Options for `compose()` function.
27
+ *
28
+ * If this is not specified, or is any value other than an `object`,
29
+ * we will look for defaults in a `composeOptions` static property:
30
+ *
31
+ * ```js
32
+ * static get composeOptions() { return {your: default, options: here}; }
33
+ * ```
34
+ *
35
+ * @param {(object|true)} [staticOpts] Static options.
36
+ *
37
+ * If this is set we'll use `composeFully()` instead of using `compose()`.
38
+ *
39
+ * If this value is an `object` it will be used as the `staticOpts`.
40
+ *
41
+ * If this is the special value `true`, then we will look for the options
42
+ * in a `staticOptions` static property:
43
+ *
44
+ * ```js
45
+ * static get staticOptions() { return {your: static, options: here}; }
46
+ * ```
47
+ *
48
+ * If this any value other than an `object` or `true`, it will be ignored
49
+ * entirely, and the regular `compose()` call will be used.
50
+ *
51
+ * @returns {object} Return value from the `setupTrait()` static method.
52
+ *
53
+ */
54
+ static composeInto(target, protoOpts, staticOpts)
55
+ {
56
+ if (!isObj(protoOpts))
57
+ {
58
+ protoOpts = this.composeOptions ?? {};
59
+ }
60
+
61
+ if (staticOpts === true)
62
+ {
63
+ staticOpts = this.staticOptions ?? {};
64
+ }
65
+
66
+ let composed;
67
+
68
+ if (isObj(staticOpts))
69
+ {
70
+ composed = composeFully(target, this, protoOpts, staticOpts);
71
+ }
72
+ else
73
+ {
74
+ composed = compose(target, this, protoOpts);
75
+ }
76
+
77
+ return this.setupTrait({target, protoOpts, staticOpts, composed});
78
+ }
79
+
80
+ /**
81
+ * A static method called by `composeInto()`
82
+ * _after_ composing the trait properties into the target.
83
+ *
84
+ * @param {object} info - Metadata from `composeInto()`
85
+ * @param {(function|object)} info.target - The `target` argument
86
+ * @param {object} info.protoOpts - The `protoOpts` used
87
+ * @param {object} [info.staticOpts] The `staticOpts` if used
88
+ * @param {module:@lumjs/core/traits~Composed} info.composed
89
+ * The return value from `compose()` or `composeFully()`.
90
+ *
91
+ * @returns {object} The `info` object, with any changes made
92
+ * by an overridden `setupTrait()` method in the sub-class.
93
+ *
94
+ * The default implementation is a placeholder that returns the
95
+ * `info` object without making any changes.
96
+ *
97
+ */
98
+ static setupTrait(info)
99
+ {
100
+ if (this.debug)
101
+ {
102
+ console.debug(this.name, "setupTrait()", info, this);
103
+ }
104
+ return info;
105
+ }
106
+
107
+ /**
108
+ * A method wrapping {@link module:@lumjs/core/traits.decompose}
109
+ * where the `source` is always the Trait sub-class constructor.
110
+ *
111
+ * See the `decompose()` docs for descriptions of the other arguments.
112
+ *
113
+ * @param {(function|object)} target
114
+ * @param {object} [opts]
115
+ * @returns {object} Return value from the `removedTrait()` static method.
116
+ */
117
+ static decomposeFrom(target, opts)
118
+ {
119
+ const info = {target, ok:true};
120
+ info.composed = this.getComposed(target);
121
+ this.removeTrait(info);
122
+
123
+ if (info.ok)
124
+ {
125
+ info.count = decompose(target, this, opts);
126
+ }
127
+
128
+ return this.removedTrait(info);
129
+ }
130
+
131
+ /**
132
+ * A static method called by `decomposeFrom()`
133
+ * _before_ decomposing the trait properties from the target.
134
+ *
135
+ * @param {object} info - Metadata from `decomposeFrom()`
136
+ * @param {(function|object)} info.target - The `target` argument
137
+ * @param {module:@lumjs/core/traits~Composed} info.composed
138
+ * The property map that was previously composed.
139
+ * @param {boolean} info.ok - Will always be `true` initially.
140
+ *
141
+ * If an overridden `removeTrait()` method sets this to `false`,
142
+ * then the decomposeFrom() operation will skip the step of
143
+ * actually decomposing the trait.
144
+ *
145
+ * @returns {*} Return value is not used.
146
+ */
147
+ static removeTrait(info)
148
+ {
149
+ if (this.debug)
150
+ {
151
+ console.debug(this.name, "removeTrait()", info, this);
152
+ }
153
+ return info;
154
+ }
155
+
156
+ /**
157
+ * A static method called by `decomposeFrom()`
158
+ * _after_ decomposing the trait properties from the target.
159
+ *
160
+ * @param {object} info - The same as `removeTrait()`, plus:
161
+ * @param {number} info.count - The number of properties decomposed;
162
+ *
163
+ * @returns {object} The `info` object, with any changes made
164
+ * by `removeTrait()` and `removedTrait()` methods in the
165
+ * sub-class.
166
+ *
167
+ * The default implementation is a placeholder that returns the
168
+ * `info` object without making any changes.
169
+ *
170
+ */
171
+ static removedTrait(info)
172
+ {
173
+ if (this.debug)
174
+ {
175
+ console.debug(this.name, "removedTrait()", info, this);
176
+ }
177
+ return info;
178
+ }
179
+
180
+ /**
181
+ * A method wrapping {@link module:@lumjs/core/traits.getComposed}
182
+ * where the `source` is always the Trait sub-class constructor.
183
+ *
184
+ * @param {(function|object)} target
185
+ * @returns {mixed} Return value from `getComposed()`
186
+ */
187
+ static getComposed(target)
188
+ {
189
+ return getComposed(target, this);
190
+ }
191
+
192
+ } // CoreTrait class
193
+
194
+ module.exports = CoreTrait;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumjs/core",
3
- "version": "1.35.1",
3
+ "version": "1.37.0",
4
4
  "main": "lib/index.js",
5
5
  "exports":
6
6
  {