@lumjs/core 1.38.4 → 1.38.6

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.
package/lib/env.js ADDED
@@ -0,0 +1,937 @@
1
+ 'use strict';
2
+
3
+ const cp = Object.assign;
4
+ const ctx = require('./context');
5
+ const { df, lazy } = require('./obj');
6
+ const { isObj } = require('./types');
7
+ const NS = '@lumjs/core/env';
8
+ const DataCache = Symbol(NS+'~DataCache');
9
+ const Metadata = Symbol(NS+'~Metadata');
10
+
11
+ const PREV_FE = 'prevItem';
12
+ const PREV_WE = 'prevCall';
13
+
14
+ // TODO: write tests.
15
+
16
+ // <Private> Callback type check.
17
+ function needCB(cb)
18
+ {
19
+ if (typeof cb !== 'function')
20
+ {
21
+ throw new TypeError("callback must be a function");
22
+ }
23
+ }
24
+
25
+ const isJsonString = (v) => (typeof v === 'string'
26
+ && ((v.startsWith('{') && v.endsWith('}'))
27
+ || (v.startsWith('[') && v.endsWith(']'))
28
+ ));
29
+
30
+ /**
31
+ * Static object containing methods that will be added
32
+ * to every forEach() Item object.
33
+ * @protected
34
+ * @alias module:@lumjs/core/env.ItemMethods
35
+ */
36
+ const ItemMethods =
37
+ {
38
+ /**
39
+ * Method used to run callback functions.
40
+ * @protected
41
+ * @param {module:@lumjs/core/env~ItemCallback} cb - Callback.
42
+ * @param {string} log - Log name for return value info.
43
+ * - The forEach() method uses `prevItem` for its (top-level) callbacks.
44
+ * - The withEach() method uses `prevCall` for its (2nd-level) callbacks.
45
+ * @returns {mixed} The actual return value from the callback.
46
+ */
47
+ callBack(cb, log)
48
+ {
49
+ let status = this.info.status = {cb, item: this};
50
+ status.retval = cb.call(this.env, this);
51
+
52
+ if (log)
53
+ {
54
+ this.info[log] = status;
55
+ }
56
+
57
+ return status.retval;
58
+ },
59
+ }
60
+
61
+ /**
62
+ * Pre-defined value handlers and helpers.
63
+ * @type {object}
64
+ * @alias module:@lumjs/core/env.Handlers
65
+ */
66
+ const Handlers =
67
+ {
68
+ /**
69
+ * A default pattern used to match strings representing
70
+ * a boolean `false` value. This version matches:
71
+ * `false`, `n`, `no`, and `off` (all case-insensitive).
72
+ * @type {RegExp}
73
+ */
74
+ falsey: /^(?:false|no?|off)$/i,
75
+
76
+ /**
77
+ * A minimalistic value handler for the get() method.
78
+ *
79
+ * This is a simple implementation that does the following:
80
+ * - If the value is the string `'null'` returns the literal `null`.
81
+ * The stored string may be overridden using `opts.nulls`.
82
+ * - If the value can be type-cast to a `number`, returns the number.
83
+ * - If a value matches `opts.falsey`, returns boolean `false`.
84
+ * - Anything else is returned as-is.
85
+ *
86
+ * Notes:
87
+ * - The string `'0'` will be returned as the number `0`, which is
88
+ * considered `false` in a boolean context; therefore it is a valid
89
+ * value to use in your environment variables to represent false.
90
+ * The other falsey strings are supported as common alternatives.
91
+ * - There are no specific tests for a boolean true value, as JS treats
92
+ * all non-empty strings as true.
93
+ *
94
+ * If you want to call this from your own custom getHandler, you should
95
+ * do so _after_ your own tests using the `call` method:
96
+ *
97
+ * ```js
98
+ * // after your tests fall back on defaults
99
+ * return env.Handlers.getDefault.call(this, value, info);
100
+ * ```
101
+ *
102
+ * @type {module:@lumjs/core/env~ValueHandler}
103
+ * @alias module:@lumjs/core/env.Handlers.getDefault
104
+ */
105
+ getDefault(v,i)
106
+ {
107
+ let nulls = i.opts.nulls ?? 'null';
108
+ if (v === nulls)
109
+ {
110
+ return null;
111
+ }
112
+
113
+ let nv = +v;
114
+ if (Number.isFinite(nv))
115
+ {
116
+ return nv;
117
+ }
118
+
119
+ let falsey = i.opts.falsey ?? Handlers.falsey;
120
+ if (falsey.test(v))
121
+ {
122
+ return false;
123
+ }
124
+
125
+ return v;
126
+ },
127
+
128
+ /**
129
+ * A minimalistic value handler for the set() method.
130
+ *
131
+ * The values it can handle are:
132
+ *
133
+ * | JS Value | Uses | Overriden using |
134
+ * | ---------| -------- | --------------- |
135
+ * | `false` | `'0'` | `opts.falses` |
136
+ * | `true` | `'1'` | `opts.trues` |
137
+ * | `null` | `'null'` | `opts.nulls` |
138
+ *
139
+ * Anything else is returned as-is.
140
+ *
141
+ * @type {module:@lumjs/core/env~ValueHandler}
142
+ * @alias module:@lumjs/core/env.Handlers.setDefault
143
+ */
144
+ setDefault(v,i)
145
+ {
146
+ if (v === null) return i.opts.nulls ?? 'null';
147
+ if (v === true) return i.opts.trues ?? '1';
148
+ if (v === false) return i.opts.falses ?? '0';
149
+ return v;
150
+ },
151
+ }
152
+
153
+ /**
154
+ * Default options for the env.* methods.
155
+ *
156
+ * - cache: `1`
157
+ * - data: `1`
158
+ * - dataSave: `'save'`
159
+ * - empty: `false`
160
+ * - falsey: `Handlers.falsey`
161
+ * - getHandler: `Handlers.getDefault`
162
+ * - setHandler: `Handlers.setDefault`
163
+ * - trim: `true`
164
+ * - wrapJSON: `true`
165
+ *
166
+ * @alias module:@lumjs/core/env.DefaultOpts
167
+ */
168
+ const DefaultOpts =
169
+ {
170
+ [Metadata]: {},
171
+ cache: 1,
172
+ data: 1,
173
+ dataSave: 'save',
174
+ empty: false,
175
+ falsey: Handlers.falsey,
176
+ getHandler: Handlers.getDefault,
177
+ setHandler: Handlers.setDefault,
178
+ trim: true,
179
+ wrapJSON: true,
180
+ }
181
+
182
+ /**
183
+ * Get a compiled options object with the defaults and metadata composed.
184
+ *
185
+ * You may use this if you're going to be using the same options for
186
+ * multiple calls. Is this premature optimisation? Likely. Do I care? Nope.
187
+ *
188
+ * @param {object} [opts] Options you want to set explicitly.
189
+ * @returns {object}
190
+ * @alias module:@lumjs/core/env.getOpts
191
+ */
192
+ function getOpts(opts)
193
+ {
194
+ return isObj(opts) && opts[Metadata] ? opts : cp({}, DefaultOpts, opts);
195
+ }
196
+
197
+ /**
198
+ * A wrapper around various environment storage systems.
199
+ *
200
+ * The storage system used depends on the runtime environment:
201
+ * - On Node this uses `process.env`
202
+ * - On web browsers this uses `localStorage`
203
+ *
204
+ * In both cases the underlying storage is assumed to use strings
205
+ * for both keys and values, and this module provides conversion
206
+ * functions for many common data types you might want to store.
207
+ *
208
+ * @exports module:@lumjs/core/env
209
+ */
210
+ exports = module.exports =
211
+ {
212
+ [DataCache]: new Map(),
213
+ Handlers,
214
+ Metadata,
215
+ DefaultOpts,
216
+ ItemMethods,
217
+ $$: {DataCache},
218
+ getOpts,
219
+
220
+ /**
221
+ * Get a value from the store.
222
+ *
223
+ * @param {string} key - Key to get.
224
+ * @param {object} [opts] Options.
225
+ *
226
+ * The `DefaultOpts` property is used to determine default values
227
+ * for any options that are not specified explicitly.
228
+ *
229
+ * Further options may be added to be used by value handlers.
230
+ *
231
+ * @param {number} [opts.cache] What values should be cached?
232
+ *
233
+ * Each level increases what is saved and includes the previous level(s).
234
+ *
235
+ * - `0` : Don't cache anything.
236
+ * - `1` : Cache data objects.
237
+ * - `2` : Cache any valid values found in storage.
238
+ * - `3` : Cache defined default values.
239
+ *
240
+ * @param {number} [opts.data] Check for embedded JSON data?
241
+ *
242
+ * - `0` : Don't support JSON at all.
243
+ * - `1` : Test string values for JSON, if no JSON, use value as is.
244
+ * - `2` : Test string values for JSON, if no JSON, use default value.
245
+ * - `3` : Test string values for JSON, if no JSON, throw a TypeError.
246
+ *
247
+ * If enabled, when a string value starts with `{` and ends with `}`,
248
+ * or starts with `[` and ends with `]`, we will attempt to parse it as JSON
249
+ * and if it parses into an object, use that object as the value.
250
+ *
251
+ * @param {?string} [opts.dataSave] Add a save() method to data objects?
252
+ *
253
+ * If this is set to a non-empty string then a method with that name will
254
+ * be added to data objects. Note: if an existing property has the name
255
+ * specified here, an error will be thrown declaring the conflict!
256
+ *
257
+ * The added method is a wrapper around env.set() that will use the options
258
+ * passed to get() as defaults, so if you have a custom `opts.revive`,
259
+ * you should probably pass the corresponding `opts.replace` as well.
260
+ *
261
+ * If you set this to a blank string or null then no method will be assigned.
262
+ *
263
+ * @param {mixed} [opts.default] Default value.
264
+ *
265
+ * Used if `key` was not found in the underlying data store.
266
+ * It may also be used if the key was found but wasn't a valid value.
267
+ *
268
+ * If this is a `function`, it will be called with `this` module
269
+ * as the context object, and passed the `key` and `opts` arguments.
270
+ *
271
+ * If it is not specified the default value will be undefined.
272
+ *
273
+ * @param {boolean} [opts.empty] Do empty strings count?
274
+ *
275
+ * If this is false, then empty strings will be considered the same as if
276
+ * the key had not been set at all.
277
+ *
278
+ * If this is true, then empty strings will be counted as set.
279
+ *
280
+ * @param {module:@lumjs/core/env~ValueHandler} [opts.getHandler]
281
+ * @param {boolean} [opts.refresh] Skip the cache?
282
+ *
283
+ * If this is set to true then the cache won't be checked for the key,
284
+ * and the value will always be looked up from the live environment.
285
+ *
286
+ * @param {function} [opts.revive] A JSON reviver.
287
+ *
288
+ * Only used if `opts.data` is true and the value matched one of the
289
+ * patterns used to check for JSON data strings.
290
+ *
291
+ * If `opts.wrapJSON` is true then this function will be called as
292
+ * a ValueHandler callback from an anonymous reviver function;
293
+ * otherwise it is directly used as the reviver argument.
294
+ *
295
+ * @param {boolean} [opts.trim] Trim leading/trailing whitespace?
296
+ *
297
+ * This option directly affects `opts.empty`, as if a value consists of
298
+ * only whitespace, the trimmed version will be an empty string.
299
+ *
300
+ * @param {boolean} [opts.wrapJSON] Wrap the reviver for extended context?
301
+ * @returns {mixed} Return value depends on if the key was found,
302
+ * the default value specified, and the various options.
303
+ *
304
+ * The underlying data stores use string values, so if the key was
305
+ * found and associated with a valid value, this will return a string.
306
+ */
307
+ get(key, opts)
308
+ {
309
+ opts = getOpts(opts);
310
+
311
+ if (!opts.refresh && this[DataCache].has(key))
312
+ {
313
+ return this[DataCache].get(key);
314
+ }
315
+
316
+ let info = {key, opts, env: this};
317
+
318
+ let value = this.getStr(key),
319
+ cache = false;
320
+
321
+ if (typeof value === 'string')
322
+ {
323
+ value = this.getParsed(value, info);
324
+ }
325
+
326
+ if (value === undefined)
327
+ {
328
+ value = (typeof opts.default === 'function')
329
+ ? opts.default.call(this, key, opts)
330
+ : opts.default;
331
+ }
332
+
333
+ if (isObj(value))
334
+ {
335
+ cache = (opts.cache > 0);
336
+ this.extendObject(value, info);
337
+ }
338
+ else if (value !== undefined)
339
+ {
340
+ cache = (opts.cache > 1);
341
+ }
342
+
343
+ if (cache)
344
+ {
345
+ this[DataCache].set(key, value);
346
+ }
347
+
348
+ return value;
349
+ },
350
+
351
+ /**
352
+ * A semi-private sub-method that parses strings into other JS values.
353
+ *
354
+ * It currently handles the following options:
355
+ * `trim`, `empty`, `getHandler`, `data`, `revive`, and `wrapJSON`.
356
+ * See {@link module:@lumjs/core/env.get get()} for details.
357
+ *
358
+ * Not meant to be called from outside get() or load() methods;
359
+ * assumes all arguments are already validated/compiled/etc.
360
+ * @protected
361
+ * @param {string} value - String value to be parsed.
362
+ * @param {module:@lumjs/core/env~ContextInfo} info - Context info.
363
+ * @returns {mixed} Value after conversion; undefined if no valid value found.
364
+ */
365
+ getParsed(value, info)
366
+ {
367
+ let {opts} = info;
368
+
369
+ if (opts.trim)
370
+ {
371
+ value = value.trim();
372
+ }
373
+ if (!opts.empty && value === '')
374
+ { // No empty strings allowed.
375
+ return undefined;
376
+ }
377
+
378
+ if (typeof opts.getHandler === 'function')
379
+ {
380
+ value = opts.getHandler.call(info, value, info);
381
+ }
382
+
383
+ if (opts.data)
384
+ { // Look for JSON data.
385
+ let foundJSON = false, log = opts.log;
386
+
387
+ if (isJsonString(value))
388
+ {
389
+ let reviver = opts.revive ?? opts.jsonRevive;
390
+ if (opts.wrapJSON && typeof reviver === 'function')
391
+ {
392
+ let revive = reviver;
393
+ reviver = function(key, value, ctx)
394
+ {
395
+ info.json = {key, ctx, target: this};
396
+ return revive.call(info, value, info);
397
+ }
398
+ }
399
+
400
+ try
401
+ {
402
+ let jd = JSON.parse(value, reviver);
403
+ if (isObj(jd))
404
+ {
405
+ value = jd;
406
+ foundJSON = true;
407
+ }
408
+ }
409
+ catch (e)
410
+ {
411
+ if (log)
412
+ {
413
+ console.error(e, {value, ...info});
414
+ log = false;
415
+ }
416
+ }
417
+ }
418
+
419
+ if (!foundJSON && opts.data > 1)
420
+ { // Didn't find JSON data but it was expected.
421
+ if (log) console.warn({value, ...info});
422
+ value = undefined;
423
+ if (opts.data > 2)
424
+ {
425
+ throw new TypeError("Invalid JSON data");
426
+ }
427
+ }
428
+ }
429
+
430
+ return value;
431
+ },
432
+
433
+ /**
434
+ * A semi-private sub-method that can add special methods and properties
435
+ * to data objects depending on the options.
436
+ *
437
+ * It currently handles the following options: `dataSave`.
438
+ * See {@link module:@lumjs/core/env.get get()} for details.
439
+ *
440
+ * Not meant to be called from outside get() or load() methods;
441
+ * assumes all arguments are already validated/compiled/etc.
442
+ * @protected
443
+ * @param {object} value - Data value object to be extended.
444
+ * @param {module:@lumjs/core/env~ContextInfo} info - Context info.
445
+ * @returns {void}
446
+ */
447
+ extendObject(value, info)
448
+ {
449
+ let {opts} = info;
450
+
451
+ if (typeof opts.dataSave === 'string')
452
+ { // Add a save() method.
453
+ let meth = opts.dataSave;
454
+ if (value[meth] !== undefined)
455
+ {
456
+ console.error({key, opts, data: value});
457
+ throw new RangeError(meth+' property already exists');
458
+ }
459
+
460
+ df(value, meth,
461
+ {
462
+ value(lopts)
463
+ {
464
+ return exports.set(key, this, cp({}, opts, lopts));
465
+ },
466
+ });
467
+ }
468
+ },
469
+
470
+ /**
471
+ * Set a value in the data store.
472
+ *
473
+ * @param {string} key - Key to set.
474
+ * @param {mixed} value - Value to set.
475
+ * @param {object} [opts] Options.
476
+ *
477
+ * @param {boolean} [opts.cache=1] What values should be cached?
478
+ *
479
+ * Each level increases what is saved and includes the previous level(s).
480
+ *
481
+ * - `0` : Don't cache anything.
482
+ * - `1` : Cache objects (default).
483
+ * - `2` : Cache any defined values.
484
+ * - `9` : Cache-only mode (don't save back to underlying storage).
485
+ *
486
+ * The last level is only for very specific use cases and not generally
487
+ * recommended unless you really know what you're doing!
488
+ *
489
+ * @param {module:@lumjs/core/env~ValueHandler} [opts.setHandler]
490
+ * @param {(function|Array)} [opts.replace] A JSON replacer.
491
+ *
492
+ * This is only used if `value` is an `object`.
493
+ *
494
+ * If `opts.wrapJSON` is true AND this is a `function`, it will be called as
495
+ * a ValueHandler callback from an anonymous replacer function.
496
+ * In any other case it is directly used as the replacer argument.
497
+ *
498
+ * @param {boolean} [opts.wrapJSON] Wrap the replacer for extended context?
499
+ * @returns {module:@lumjs/core/env~SetInfo}
500
+ */
501
+ set(key, value, opts)
502
+ {
503
+ opts = getOpts(opts);
504
+
505
+ let info = {key, value, opts, ok: true, env: this};
506
+
507
+ if (value !== undefined
508
+ && (opts.cache > 1
509
+ || (opts.cache > 0 && isObj(value))))
510
+ {
511
+ this[DataCache].set(key, value);
512
+ }
513
+
514
+ if (opts.cache >= 9)
515
+ { // Cache-only mode is in use.
516
+ return info;
517
+ }
518
+
519
+ if (typeof opts.setHandler === 'function')
520
+ {
521
+ value = opts.setHandler.call(info, value);
522
+ }
523
+
524
+ if (isObj(value))
525
+ {
526
+ let json = null,
527
+ replacer = opts.replace ?? opts.jsonReplace;
528
+
529
+ if (opts.wrapJSON && typeof replacer === 'function')
530
+ {
531
+ let replace = replacer;
532
+ replacer = function(key, value)
533
+ {
534
+ info.json = {key, target: this};
535
+ return replace.call(info, value, info);
536
+ }
537
+ }
538
+
539
+ try
540
+ {
541
+ json = JSON.stringify(value, replacer);
542
+ }
543
+ catch (e)
544
+ {
545
+ console.error(e);
546
+ info.error = e;
547
+ }
548
+
549
+ info.json = json;
550
+
551
+ if (typeof json === 'string')
552
+ {
553
+ value = json;
554
+ }
555
+ else
556
+ {
557
+ info.ok = false;
558
+ return info;
559
+ }
560
+ }
561
+
562
+ this.setStr(key, value.toString());
563
+ return info;
564
+ },
565
+
566
+ /**
567
+ * A semi-private sub-method used by load().
568
+ *
569
+ * - First calls getParsed() for `string` values.
570
+ * - Then extendObject() for `object` values.
571
+ * - Finally calls set() with the final value.
572
+ * @protected
573
+ * @param {string} key
574
+ * @param {mixed} value
575
+ * @param {object} opts
576
+ * @returns {module:@lumjs/core/env~SetInfo}
577
+ */
578
+ setSource(key, value, opts)
579
+ {
580
+ let info = {key, opts, env: this};
581
+
582
+ if (typeof value === 'string')
583
+ {
584
+ value = this.getParsed(value, info);
585
+ }
586
+
587
+ if (isObj(value))
588
+ {
589
+ this.extendObject(value, info);
590
+ }
591
+
592
+ return this.set(key, value, opts);
593
+ },
594
+
595
+ /**
596
+ * Load extra environment variables.
597
+ *
598
+ * This is meant for packages like @lumjs/envfile and @lumjs/dotjs
599
+ * to be able to load environment variables from a file.
600
+ *
601
+ * @param {object} values - Variables to load.
602
+ *
603
+ * This may be a Map instance, just noting that only string keys are
604
+ * supported by the underlying storage systems. Value types aren't as
605
+ * important and may either be any JS type supported by set(), or any
606
+ * of the string formats supported by get().
607
+ *
608
+ * If this is any other type of object, all enumerable properties with
609
+ * string keys will be used as the key/value pairs.
610
+ *
611
+ * @param {object} [opts] Options.
612
+ * @returns {Map} The return values from each set() call.
613
+ */
614
+ load(values, opts)
615
+ {
616
+ if (!isObj(values))
617
+ {
618
+ throw new TypeError("values must be an object");
619
+ }
620
+
621
+ opts = getOpts(opts);
622
+
623
+ let stats = new Map();
624
+ let sets = (k,v) => stats.set(k, this.setSource(k, v, opts));
625
+
626
+ if (values instanceof Map)
627
+ {
628
+ for (let [key,val] of values)
629
+ {
630
+ sets(key,val);
631
+ }
632
+ }
633
+ else
634
+ {
635
+ for (let key in values)
636
+ {
637
+ sets(key, values[key]);
638
+ }
639
+ }
640
+
641
+ return stats;
642
+ },
643
+
644
+ /**
645
+ * Remove a key from the cache (if it was cached).
646
+ * @param {string} key - Key to remove from cache.
647
+ * @returns {object} this module.
648
+ */
649
+ uncache(key)
650
+ {
651
+ this[DataCache].delete(key);
652
+ return this;
653
+ },
654
+
655
+ /**
656
+ * Remove a key from the underlying storage.
657
+ * @function module:@lumjs/core/env.unset
658
+ * @param {string} key - Key to remove from storage.
659
+ * @returns {object} this module.
660
+ */
661
+
662
+ /**
663
+ * Calls both uncache() and unset() at once.
664
+ * @param {string} key - Key to remove.
665
+ * @returns {object} this.module
666
+ */
667
+ remove(key)
668
+ {
669
+ return this.uncache(key).unset(key);
670
+ },
671
+
672
+ /**
673
+ * Clear the data cache.
674
+ *
675
+ * This removes all items from the cache.
676
+ * It has no affect whatsoever on the underlying storage.
677
+ *
678
+ * @returns {object} this module.
679
+ */
680
+ clear()
681
+ {
682
+ this[DataCache].clear();
683
+ return this;
684
+ },
685
+
686
+ /**
687
+ * Call a callback function for every storage item.
688
+ * @function module:@lumjs/core/env.forEach
689
+ * @param {module:@lumjs/core/env~ItemCallback} cb - Callback function.
690
+ * @param {object} [opts] Options for get() method.
691
+ *
692
+ * The _item object_ passed to the callback has a lazy-loaded property
693
+ * called `value` that calls `get(key, opts)` the first time its accessed.
694
+ *
695
+ * The options are also available as an `opts` property in the item object,
696
+ * so the callbacks themselves may use them to specify their own options.
697
+ *
698
+ * @returns {object} Final compiled options.
699
+ *
700
+ * Composes passed `opts` with the get() default options and includes
701
+ * an `env.Metadata` symbol property with information about the process.
702
+ */
703
+
704
+ /**
705
+ * A wrapper around forEach() that can chain nested callback functions.
706
+ *
707
+ * If more than one callback is specified, this generates an implicit
708
+ * forEach() callback which calls all of the callback arguments in the
709
+ * order they were specified in.
710
+ *
711
+ * If one of the child callback functions returns boolean `false`, or an
712
+ * Error instance, no further callbacks will be called with that item.
713
+ *
714
+ * If only ONE callback is specified, it will be passed directly to the
715
+ * forEach() method, skipping any of the chaining logic entirely.
716
+ *
717
+ * If NO callbacks are specified, an error will be thrown!
718
+ *
719
+ * @param {?object} opts - Options to be passed to forEach() method.
720
+ * @param {...module:@lumjs/core/env~ItemCallback} callbacks - Callbacks.
721
+ * @returns {object} Output from forEach()
722
+ * @throws {RangeError} If no callbacks were specified.
723
+ */
724
+ withEach(opts, ...callbacks)
725
+ {
726
+ if (callbacks.length === 0)
727
+ {
728
+ throw new RangeError("at least one callback must be specified");
729
+ }
730
+
731
+ if (callbacks.length === 1)
732
+ {
733
+ return this.forEach(callbacks[0], opts);
734
+ }
735
+
736
+ return this.forEach((item) =>
737
+ {
738
+ let retval;
739
+ for (let cb of callbacks)
740
+ {
741
+ if (typeof cb === 'function')
742
+ {
743
+ retval = item.callBack(cb, PREV_WE);
744
+ if (retval === false || retval instanceof Error)
745
+ { // We're done here.
746
+ return retval;
747
+ }
748
+ }
749
+ else
750
+ {
751
+ console.error("invalid callback", cb, {opts, callbacks});
752
+ }
753
+ }
754
+ return retval;
755
+ }, opts);
756
+ },
757
+
758
+ /**
759
+ * A simplified withEach() wrapper that has an API closer to forEach(),
760
+ * but supports an additional filter callback option.
761
+ *
762
+ * This is primarily used by the getMap() and getProps() methods.
763
+ *
764
+ * @param {module:@lumjs/core/env~ItemCallback} main - Main callback.
765
+ * @param {object} [opts] Options.
766
+ *
767
+ * In addition to the `filter` option specific to this method,
768
+ * see {@link module:@lumjs/core/env.forEach forEach()} and
769
+ * {@link module:@lumjs/core/env.get get()} for more supported options.
770
+ *
771
+ * @param {module:@lumjs/core/env~ItemCallback} [opts.filter] Filter.
772
+ *
773
+ * If this callback is specified, it will be called _before_ `main`,
774
+ * and its return value will determine which items will actually be
775
+ * passed to the main callback.
776
+ */
777
+ getWith(main, opts)
778
+ {
779
+ opts = getOpts(opts);
780
+ let callbacks = [main];
781
+ if (typeof opts.filter === 'function')
782
+ {
783
+ callbacks.unshift(opts.filter);
784
+ }
785
+ return this.withEach(opts, ...callbacks);
786
+ },
787
+
788
+ /**
789
+ * Get a Map containing all keys/values in the environment storage.
790
+ *
791
+ * See {@link module:@lumjs/core/env.getProps getProps()} for a version
792
+ * that returns a plain object instead of a Map.
793
+ *
794
+ * @param {object} [opts] Options;
795
+ * See {@link module:@lumjs/core/env.getWith getWith()} for details.
796
+ * @returns {Map}
797
+ */
798
+ getMap(opts)
799
+ {
800
+ let map = new Map();
801
+ this.getWith(item => map.set(item.key, item.value), opts);
802
+ return map;
803
+ },
804
+
805
+ /**
806
+ * Get an object containing all keys/values in the environment storage.
807
+ *
808
+ * See {@link module:@lumjs/core/env.getMap getMap()} for a version
809
+ * that returns a Map instead of a plain object.
810
+ *
811
+ * @param {object} [opts] Options;
812
+ * See {@link module:@lumjs/core/env.getWith getWith()} for details.
813
+ * @returns {object}
814
+ */
815
+ getProps(opts)
816
+ {
817
+ let props = {};
818
+ this.getWith(item => props[item.key] = item.value, opts);
819
+ return props;
820
+ },
821
+
822
+ /**
823
+ * Private method that builds the forEach() Item objects.
824
+ * @private
825
+ * @param {string} key - Key for current item.
826
+ * @param {object} opts - Compiled options.
827
+ * @returns {module:@lumjs/core/env~CallbackItem}
828
+ */
829
+ forItem(key, opts)
830
+ {
831
+ return lazy(
832
+ {
833
+ key,
834
+ opts,
835
+ info: opts[Metadata],
836
+ env: this,
837
+ ...ItemMethods,
838
+ }, 'value', () => this.get(key, opts));
839
+ },
840
+
841
+ /**
842
+ * Private method that returns a forEach() Runner object.
843
+ * No formal docs for Runners as they aren't meant for public use.
844
+ * @private
845
+ * @param {module:@lumjs/core/env~ItemCallback} cb - Callback.
846
+ * @param {object} [opts] Options.
847
+ * @returns {object} Runner object. Has two methods:
848
+ * - `run(key)` → Called once for every key in the storage.
849
+ * - `done()` → Called when all keys have been handled.
850
+ */
851
+ forRunner(cb, opts)
852
+ {
853
+ needCB(cb);
854
+ opts = getOpts(opts);
855
+ let env = this;
856
+ return {
857
+ run(key)
858
+ {
859
+ env.forItem(key, opts).callBack(cb, PREV_FE);
860
+ },
861
+ done()
862
+ {
863
+ delete opts[Metadata].status;
864
+ return opts;
865
+ }
866
+ };
867
+ }
868
+ }
869
+
870
+ /**
871
+ * Alias of remove() method.
872
+ * @param {string} key - Key to delete.
873
+ * @returns {object} this
874
+ */
875
+ exports.delete = exports.remove;
876
+
877
+ // Now the platform-specific methods are added!
878
+
879
+ if (ctx.isNode)
880
+ {
881
+ exports.getStr = function(key)
882
+ {
883
+ return process.env[key];
884
+ }
885
+
886
+ exports.setStr = function(key, value)
887
+ {
888
+ process.env[key] = value;
889
+ return this;
890
+ }
891
+
892
+ // I don't know if that's gonna do much, but anyway...
893
+ exports.unset = function(key)
894
+ {
895
+ delete process.env[key];
896
+ }
897
+
898
+ exports.forEach = function(cb, opts)
899
+ {
900
+ cb = this.forRunner(cb, opts);
901
+ for (let key in process.env)
902
+ {
903
+ cb.run(key);
904
+ }
905
+ return cb.done();
906
+ }
907
+ }
908
+ else if (ctx.isBrowser)
909
+ {
910
+ exports.getStr = function(key)
911
+ { // getItem returns null for no value, but we want undefined.
912
+ return localStorage.getItem(key) ?? undefined;
913
+ }
914
+
915
+ exports.setStr = function(key, value)
916
+ {
917
+ localStorage.setItem(key, value);
918
+ return this;
919
+ }
920
+
921
+ exports.unset = function(key)
922
+ {
923
+ localStorage.removeItem(key);
924
+ return this;
925
+ }
926
+
927
+ exports.forEach = function(cb, opts)
928
+ {
929
+ cb = this.forRunner(cb, opts);
930
+ for (let i=0; i < localStorage.length; i++)
931
+ {
932
+ let key = localStorage.key(i);
933
+ cb.run(key);
934
+ }
935
+ return cb.done();
936
+ }
937
+ }