@tetherto/mdk-core 0.0.0 → 0.12.1

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.
Files changed (54) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +572 -0
  3. package/index.js +519 -0
  4. package/lib/address-policy.js +124 -0
  5. package/lib/local-discovery.js +321 -0
  6. package/lib/services/actions.service.js +68 -0
  7. package/lib/services/alerts.service.js +207 -0
  8. package/lib/services/comments.service.js +118 -0
  9. package/lib/services/log-history.service.js +307 -0
  10. package/lib/services/logs.service.js +249 -0
  11. package/lib/services/pool-utils/constants.js +20 -0
  12. package/lib/services/pool-utils/time.js +49 -0
  13. package/lib/services/pool.service.js +397 -0
  14. package/lib/services/provisioning.service.js +420 -0
  15. package/lib/services/settings.service.js +26 -0
  16. package/lib/services/snaps.service.js +181 -0
  17. package/lib/services/stats.service.js +205 -0
  18. package/lib/stats/README.md +80 -0
  19. package/lib/stats/index.js +57 -0
  20. package/lib/stats/ops/alerts_aggr.js +40 -0
  21. package/lib/stats/ops/alerts_group_cnt.js +43 -0
  22. package/lib/stats/ops/arr_concat.js +27 -0
  23. package/lib/stats/ops/array_obj_calc.js +41 -0
  24. package/lib/stats/ops/avg.js +42 -0
  25. package/lib/stats/ops/cnt.js +19 -0
  26. package/lib/stats/ops/group.js +38 -0
  27. package/lib/stats/ops/group_avg.js +53 -0
  28. package/lib/stats/ops/group_cnt.js +38 -0
  29. package/lib/stats/ops/group_max.js +40 -0
  30. package/lib/stats/ops/group_multiple_stats.js +40 -0
  31. package/lib/stats/ops/group_sum.js +40 -0
  32. package/lib/stats/ops/index.js +35 -0
  33. package/lib/stats/ops/nested_obj_concat.js +54 -0
  34. package/lib/stats/ops/obj_concat.js +33 -0
  35. package/lib/stats/ops/sum.js +27 -0
  36. package/lib/stats/utils.js +99 -0
  37. package/lib/templates/alerts.js +128 -0
  38. package/lib/templates/stats.js +476 -0
  39. package/lib/things/constants.js +65 -0
  40. package/lib/things/container.js +84 -0
  41. package/lib/things/miner.js +190 -0
  42. package/lib/things/powermeter.js +30 -0
  43. package/lib/things/sensor.js +18 -0
  44. package/lib/things/thing.js +93 -0
  45. package/lib/utils.js +603 -0
  46. package/lib/worker-infra.js +221 -0
  47. package/package.json +64 -1
  48. package/services.js +246 -0
  49. package/utils/compose-yaml.js +56 -0
  50. package/utils/constants.js +30 -0
  51. package/utils/index.js +29 -0
  52. package/utils/initialize.js +148 -0
  53. package/utils/service-bootstrap.js +271 -0
  54. package/worker.js +9 -0
@@ -0,0 +1,205 @@
1
+ 'use strict'
2
+
3
+ const debug = require('debug')('mdk:stats')
4
+ const async = require('async')
5
+ const utilsStore = require('@tetherto/hp-svc-facs-store/utils')
6
+ const gLibStats = require('../stats')
7
+
8
+ const STAT_RTD = 'stat-rtd'
9
+
10
+ /**
11
+ * Aggregation over per-device `last` snapshots. `lib` is the stats spec
12
+ * module ({ specs, conf }) passed in directly. Device state comes from
13
+ * injected accessors:
14
+ * listDeviceIds() → [deviceId]
15
+ * getDeviceMeta(id) → { id, tags, info, opts, type }
16
+ * getLast(id) → { snap, alerts, err, ts } | undefined
17
+ * getRealtimeData(id) → latest snap without a device round-trip
18
+ */
19
+ class StatsService {
20
+ constructor ({ lib, specTags, baseType, listDeviceIds, getDeviceMeta, getLast, getRealtimeData, processAlerts, logs, conf }) {
21
+ if (!logs) throw new Error('ERR_STATS_LOGS_REQUIRED')
22
+ this.lib = lib || null
23
+ this.specTags = specTags || []
24
+ this.baseType = baseType || 'thing'
25
+ this.listDeviceIds = listDeviceIds || (() => [])
26
+ this.getDeviceMeta = getDeviceMeta || (() => null)
27
+ this.getLast = getLast || (() => undefined)
28
+ this.getRealtimeData = getRealtimeData || null
29
+ this.processAlerts = processAlerts || (() => null)
30
+ this.logs = logs
31
+ this.conf = conf || {}
32
+ this._buildingStats = {}
33
+ this._collectingRtd = false
34
+ }
35
+
36
+ async buildStats (sk, fireTime) {
37
+ if (sk === STAT_RTD) {
38
+ return await this.saveRealTimeData()
39
+ }
40
+
41
+ if (!this.lib) {
42
+ return
43
+ }
44
+
45
+ const lkr = `_buildingStats_${sk}`
46
+
47
+ if (this._buildingStats[lkr]) {
48
+ return
49
+ }
50
+
51
+ this._buildingStats[lkr] = true
52
+
53
+ try {
54
+ await this.buildStatsForKey(sk, fireTime)
55
+ } finally {
56
+ this._buildingStats[lkr] = false
57
+ }
58
+ }
59
+
60
+ async saveRealTimeData () {
61
+ if (this._collectingRtd) return
62
+ if (!this.getRealtimeData) return
63
+ this._collectingRtd = true
64
+
65
+ const thgsRtd = {}
66
+
67
+ try {
68
+ await async.eachLimit(
69
+ this.listDeviceIds(),
70
+ this.conf.thingRtdConcurrency || 500,
71
+ async (deviceId) => {
72
+ const meta = this.getDeviceMeta(deviceId)
73
+ if (!meta) return
74
+ thgsRtd[deviceId] = { ...meta, last: this.getLast(deviceId) || {} }
75
+ try {
76
+ const rtd = await this.getRealtimeData(deviceId)
77
+ if (!rtd) return
78
+
79
+ const dev = { ...meta, last: this.getLast(deviceId) || {} }
80
+ thgsRtd[deviceId] = {
81
+ ...meta,
82
+ last: { snap: rtd, alerts: this.processAlerts(dev) }
83
+ }
84
+ } catch (e) {
85
+ debug('ERR_RTD %s %s', deviceId, e.message)
86
+ }
87
+ }
88
+ )
89
+ } finally {
90
+ this._collectingRtd = false
91
+ }
92
+
93
+ if (!Object.keys(thgsRtd).length) return
94
+
95
+ try {
96
+ const aggrData = this.aggrStats(Object.keys(thgsRtd), {}, thgsRtd)
97
+ const log = await this.logs.getBeeTimeLog(
98
+ `${STAT_RTD}-t-${this.baseType}`,
99
+ 0,
100
+ true
101
+ )
102
+ await log.put(STAT_RTD, Buffer.from(JSON.stringify(aggrData)))
103
+ await this.logs.releaseBeeTimeLog(log)
104
+ } catch (e) {
105
+ debug('ERR_SAVING_RTD %s', e.message)
106
+ }
107
+ }
108
+
109
+ aggrStats (thgIds, opts = {}, thgs = null) {
110
+ const acc = {}
111
+ if (!this.lib) return acc
112
+
113
+ const specs = this.lib.specs
114
+ const ids = thgIds || this.listDeviceIds()
115
+
116
+ this.specTags.forEach(stype => {
117
+ if (!specs[stype]) {
118
+ return
119
+ }
120
+
121
+ const state = {}
122
+ state.ops = specs[stype].ops
123
+
124
+ for (const thgId of ids) {
125
+ const thg = thgs ? thgs[thgId] : this._deviceEntry(thgId)
126
+ if (!thg) continue
127
+ const last = thg.last || {}
128
+ const info = thg.info || {}
129
+ gLibStats.applyStats(
130
+ state,
131
+ acc,
132
+ { last, info },
133
+ { id: thg.id, tags: thg.tags, info, opts: thg.opts, type: thg.type }
134
+ )
135
+ }
136
+
137
+ gLibStats.tallyStats(state, acc)
138
+ })
139
+
140
+ return acc
141
+ }
142
+
143
+ _deviceEntry (deviceId) {
144
+ const meta = this.getDeviceMeta(deviceId)
145
+ if (!meta) return null
146
+ return { ...meta, last: this.getLast(deviceId) || {} }
147
+ }
148
+
149
+ async buildStatsForKey (logKey, fireTime) {
150
+ const now = Math.floor(fireTime.getTime() / 1000) * 1000
151
+
152
+ // Null prototype: a stored tag named `__proto__` or `constructor` must not
153
+ // resolve to a built-in and break the build for every device.
154
+ const tagSpecs = Object.create(null)
155
+ const thgIdsAll = this.listDeviceIds()
156
+ if (!this.lib) return
157
+
158
+ const skipTagPfxs = this.lib.conf?.skipTagPrefixes || []
159
+
160
+ thgIdsAll.forEach(thgId => {
161
+ const meta = this.getDeviceMeta(thgId)
162
+ if (!meta) return
163
+
164
+ const tags = Array.isArray(meta.tags) ? meta.tags : []
165
+ tags.forEach(tag => {
166
+ if (typeof tag !== 'string' || !tag) return
167
+ if (skipTagPfxs.find(p => tag.startsWith(p))) {
168
+ return
169
+ }
170
+
171
+ if (!tagSpecs[tag]) {
172
+ tagSpecs[tag] = { thgIds: [] }
173
+ }
174
+
175
+ tagSpecs[tag].thgIds.push(thgId)
176
+ })
177
+ })
178
+
179
+ const tags = Object.keys(tagSpecs)
180
+
181
+ for (const tag of tags) {
182
+ const tagSpec = tagSpecs[tag]
183
+
184
+ const acc = this.aggrStats(tagSpec.thgIds)
185
+ const log = await this.logs.getBeeTimeLog(`${logKey}-${tag}`, 0, true)
186
+
187
+ try {
188
+ await log.put(
189
+ utilsStore.convIntToBin(now),
190
+ Buffer.from(JSON.stringify({
191
+ ts: now,
192
+ ...acc
193
+ }))
194
+ )
195
+ } catch (e) {
196
+ debug('ERR_BUILD_STATS %s %s', tag, e.message)
197
+ }
198
+
199
+ await this.logs.releaseBeeTimeLog(log)
200
+ }
201
+ }
202
+ }
203
+
204
+ module.exports = StatsService
205
+ module.exports.STAT_RTD = STAT_RTD
@@ -0,0 +1,80 @@
1
+ # Lib-stats
2
+
3
+ Telemetry aggregation library. Provides a set of composable statistical operations that device Workers use to compute time-bucketed metrics (hashrate averages, power totals, group counts, etc.).
4
+
5
+ Part of `@tetherto/mdk-core` and exported as `libStats`. It is used by `StatsService`
6
+ ([`lib/services/stats.service.js`](../services/stats.service.js)) and by each Worker plugin's `lib/templates/stats.js`, for example
7
+ [the Avalon plugin's](../../../../workers/miners/avalon/lib/templates/stats.js).
8
+
9
+ ## Usage
10
+
11
+ ```js
12
+ const { libStats } = require('@tetherto/mdk-core')
13
+ const { applyStats, tallyStats, defaults } = libStats
14
+
15
+ // Define what to compute
16
+ const state = {
17
+ ops: {
18
+ total_power: { op: 'sum', field: 'power_w' },
19
+ avg_hashrate: { op: 'avg', field: 'hashrate_mhs' },
20
+ by_container: { op: 'group_by', field: 'container' }
21
+ }
22
+ }
23
+
24
+ const pack = {}
25
+
26
+ // Feed entries one at a time
27
+ for (const entry of deviceSnapshots) {
28
+ applyStats(state, pack, entry, extData)
29
+ }
30
+
31
+ // Finalize (resolve running accumulators to result values)
32
+ tallyStats(state, pack)
33
+
34
+ // pack.total_power, pack.avg_hashrate, pack.by_container now contain results
35
+ ```
36
+
37
+ ## API
38
+
39
+ ### `applyStats(state, pack, entry, ext)`
40
+
41
+ Apply all configured operations to a single `entry`. Accumulates intermediate state in `pack`. Call once per device snapshot per time bucket.
42
+
43
+ ### `tallyStats(state, pack)`
44
+
45
+ Finalize the accumulation. Resolves running counters/sums into their final values and replaces each key in `pack` with its `.res` field.
46
+
47
+ ### `defaults.timeframes`
48
+
49
+ Array of `[label, cronExpression]` pairs for the standard time buckets:
50
+
51
+ ```js
52
+ [
53
+ ['5m', '0 */5 * * * *'],
54
+ ['30m', '0 */30 * * * *'],
55
+ ['3h', '0 0 */3 * * *'],
56
+ ['1D', '0 0 0 * * *']
57
+ ]
58
+ ```
59
+
60
+ ## Operations
61
+
62
+ | Op name | File | Description |
63
+ |---------|------|-------------|
64
+ | `cnt` | [`ops/cnt.js`](./ops/cnt.js) | Count entries |
65
+ | `sum` | [`ops/sum.js`](./ops/sum.js) | Numeric sum |
66
+ | `avg` | [`ops/avg.js`](./ops/avg.js) | Running average |
67
+ | `group` | [`ops/group.js`](./ops/group.js) | Group entries by a field |
68
+ | `group_sum` | [`ops/group_sum.js`](./ops/group_sum.js) | Sum within groups |
69
+ | `group_avg` | [`ops/group_avg.js`](./ops/group_avg.js) | Average within groups |
70
+ | `group_cnt` | [`ops/group_cnt.js`](./ops/group_cnt.js) | Count within groups |
71
+ | `group_max` | [`ops/group_max.js`](./ops/group_max.js) | Max within groups |
72
+ | `group_multiple_stats` | [`ops/group_multiple_stats.js`](./ops/group_multiple_stats.js) | Multiple aggregations per group |
73
+ | `arr_concat` | [`ops/arr_concat.js`](./ops/arr_concat.js) | Concatenate arrays across entries |
74
+ | `obj_concat` | [`ops/obj_concat.js`](./ops/obj_concat.js) | Merge objects across entries |
75
+ | `nested_obj_concat` | [`ops/nested_obj_concat.js`](./ops/nested_obj_concat.js) | Deep-merge nested objects |
76
+ | `array_obj_calc` | [`ops/array_obj_calc.js`](./ops/array_obj_calc.js) | Compute over arrays of objects |
77
+ | `alerts_aggr` | [`ops/alerts_aggr.js`](./ops/alerts_aggr.js) | Aggregate alert states |
78
+ | `alerts_group_cnt` | [`ops/alerts_group_cnt.js`](./ops/alerts_group_cnt.js) | Count alerts by group |
79
+
80
+ Each operation module exports `{ calc, tally }`. `calc` is called per entry; `tally` is called once to finalize.
@@ -0,0 +1,57 @@
1
+ 'use strict'
2
+
3
+ const { getVal, groupBy } = require('./utils')
4
+ const { getOp } = require('./ops')
5
+
6
+ function applyStats (state, pack, entry, ext) {
7
+ const opKeys = Object.keys(state.ops)
8
+
9
+ opKeys.forEach(ok => {
10
+ const op = state.ops[ok]
11
+
12
+ const met = getOp(op.op)
13
+
14
+ if (!met.calc) {
15
+ return
16
+ }
17
+
18
+ pack[ok] = met.calc(op, pack[ok], entry, ext)
19
+ })
20
+ }
21
+
22
+ function tallyStats (state, pack) {
23
+ const opKeys = Object.keys(state.ops)
24
+
25
+ opKeys.forEach(ok => {
26
+ const op = state.ops[ok]
27
+
28
+ const met = getOp(op.op)
29
+
30
+ if (!pack[ok]) {
31
+ return
32
+ }
33
+
34
+ if (met.tally) {
35
+ pack[ok] = met.tally(op, pack[ok])
36
+ }
37
+
38
+ pack[ok] = pack[ok].res
39
+ })
40
+ }
41
+
42
+ const DEFAULT_TIMEFRAMES = [
43
+ ['5m', '0 */5 * * * *'],
44
+ ['30m', '0 */30 * * * *'],
45
+ ['3h', '0 0 */3 * * *'],
46
+ ['1D', '0 0 0 * * *']
47
+ ]
48
+
49
+ module.exports = {
50
+ defaults: {
51
+ timeframes: DEFAULT_TIMEFRAMES
52
+ },
53
+ applyStats,
54
+ tallyStats,
55
+ getVal,
56
+ groupBy
57
+ }
@@ -0,0 +1,40 @@
1
+ 'use strict'
2
+
3
+ const lUtils = require('./../utils')
4
+
5
+ function alertsAggr (op, cur, entry, ext) {
6
+ if (!cur) {
7
+ cur = { res: lUtils.createResMap() }
8
+ }
9
+
10
+ if (!entry) {
11
+ return cur
12
+ }
13
+
14
+ if (op.filter && !op.filter(entry)) {
15
+ return cur
16
+ }
17
+
18
+ const groups = lUtils.getVal(entry, op.src)
19
+
20
+ if (!groups) {
21
+ return cur
22
+ }
23
+
24
+ Object.keys(groups).forEach(gk => {
25
+ if (!lUtils.isSafeKey(gk)) return
26
+ const gv = groups[gk]
27
+
28
+ if (!cur.res[gk]) {
29
+ cur.res[gk] = 0
30
+ }
31
+
32
+ cur.res[gk] += gv
33
+ })
34
+
35
+ return cur
36
+ }
37
+
38
+ module.exports = {
39
+ calc: alertsAggr
40
+ }
@@ -0,0 +1,43 @@
1
+ 'use strict'
2
+
3
+ const lUtils = require('./../utils')
4
+
5
+ function alertsGroupCnt (op, cur, entry, ext) {
6
+ if (!cur) {
7
+ cur = { res: lUtils.createResMap() }
8
+ }
9
+
10
+ if (!entry) {
11
+ return cur
12
+ }
13
+
14
+ if (op.filter && !op.filter(entry)) {
15
+ return cur
16
+ }
17
+
18
+ const alerts = lUtils.getVal(entry, op.src)
19
+
20
+ if (!alerts) {
21
+ return cur
22
+ }
23
+
24
+ alerts.forEach(al => {
25
+ if (!al.severity) {
26
+ al.severity = 'medium'
27
+ }
28
+
29
+ if (!lUtils.isSafeKey(al.severity)) return
30
+
31
+ if (!cur.res[al.severity]) {
32
+ cur.res[al.severity] = 0
33
+ }
34
+
35
+ cur.res[al.severity]++
36
+ })
37
+
38
+ return cur
39
+ }
40
+
41
+ module.exports = {
42
+ calc: alertsGroupCnt
43
+ }
@@ -0,0 +1,27 @@
1
+ 'use strict'
2
+
3
+ const lUtils = require('./../utils')
4
+
5
+ function arrConcat (op, cur, entry, ext) {
6
+ if (!cur) {
7
+ cur = { res: [] }
8
+ }
9
+
10
+ if (!entry) {
11
+ return cur
12
+ }
13
+
14
+ if (op.filter && !op.filter(entry)) {
15
+ return cur
16
+ }
17
+
18
+ const val = lUtils.getVal(entry, op.src)
19
+
20
+ if (val !== null && val !== undefined) cur.res = cur.res.concat(val)
21
+
22
+ return cur
23
+ }
24
+
25
+ module.exports = {
26
+ calc: arrConcat
27
+ }
@@ -0,0 +1,41 @@
1
+ 'use strict'
2
+
3
+ const lUtils = require('../utils')
4
+ const { getOp } = require('./index')
5
+
6
+ function arrayObjCalc (op, cur, entry, ext) {
7
+ if (!cur) {
8
+ cur = { res: lUtils.createResMap() }
9
+ }
10
+
11
+ if (!entry) {
12
+ return cur
13
+ }
14
+
15
+ if (op.filter && !op.filter(entry)) {
16
+ return cur
17
+ }
18
+
19
+ const arr = lUtils.getVal(entry, op.src)
20
+ arr?.forEach(obj => {
21
+ op.objOps.forEach(keyOp => {
22
+ if (!lUtils.isSafeKey(keyOp.src)) return
23
+ const val = lUtils.getVal(obj, keyOp.src)
24
+ if (!val) return
25
+ if (!cur.res[keyOp.src]) {
26
+ cur.res[keyOp.src] = val
27
+ return
28
+ }
29
+
30
+ const calcFn = getOp(keyOp.op)
31
+ const calcRes = calcFn.calc(keyOp, { res: cur.res[keyOp.src] }, obj)
32
+ cur.res[keyOp.src] = calcRes.res
33
+ })
34
+ })
35
+
36
+ return cur
37
+ }
38
+
39
+ module.exports = {
40
+ calc: arrayObjCalc
41
+ }
@@ -0,0 +1,42 @@
1
+ 'use strict'
2
+
3
+ const lUtils = require('./../utils')
4
+
5
+ function avg (op, cur, entry, ext) {
6
+ if (!cur) {
7
+ cur = { acc: 0, cnt: 0, res: 0 }
8
+ }
9
+
10
+ if (!entry) {
11
+ return cur
12
+ }
13
+
14
+ if (op.filter && !op.filter(entry)) {
15
+ return cur
16
+ }
17
+
18
+ const val = lUtils.getVal(entry, op.src)
19
+
20
+ cur.acc += val ? +val : 0
21
+
22
+ cur.cnt++
23
+
24
+ return cur
25
+ }
26
+
27
+ function tally (op, cur) {
28
+ if (cur.cnt) {
29
+ cur = {
30
+ res: cur.acc / cur.cnt
31
+ }
32
+ } else {
33
+ cur = { res: 0 }
34
+ }
35
+
36
+ return cur
37
+ }
38
+
39
+ module.exports = {
40
+ calc: avg,
41
+ tally
42
+ }
@@ -0,0 +1,19 @@
1
+ 'use strict'
2
+
3
+ function cnt (op, cur, entry, ext) {
4
+ if (!cur) {
5
+ cur = { res: 0 }
6
+ }
7
+
8
+ if (op.filter && !op.filter(entry)) {
9
+ return cur
10
+ }
11
+
12
+ cur.res++
13
+
14
+ return cur
15
+ }
16
+
17
+ module.exports = {
18
+ calc: cnt
19
+ }
@@ -0,0 +1,38 @@
1
+ 'use strict'
2
+
3
+ const lUtils = require('./../utils')
4
+
5
+ function group (op, cur, entry, ext) {
6
+ if (!cur) {
7
+ cur = { res: lUtils.createResMap() }
8
+ }
9
+
10
+ if (!entry) {
11
+ return cur
12
+ }
13
+
14
+ if (!op.src) {
15
+ return cur
16
+ }
17
+
18
+ if (op.filter && !op.filter(entry)) {
19
+ return cur
20
+ }
21
+
22
+ if (!op.group) {
23
+ return cur
24
+ }
25
+ const groupName = op.group(entry, ext)
26
+
27
+ if (!groupName || !lUtils.isSafeKey(groupName)) {
28
+ return cur
29
+ }
30
+
31
+ cur.res[groupName] = lUtils.getVal(entry, op.src)
32
+
33
+ return cur
34
+ }
35
+
36
+ module.exports = {
37
+ calc: group
38
+ }
@@ -0,0 +1,53 @@
1
+ 'use strict'
2
+
3
+ const lUtils = require('../utils')
4
+
5
+ function groupAvg (op, cur, entry, ext) {
6
+ if (!cur) {
7
+ cur = { res: lUtils.createResMap() }
8
+ }
9
+
10
+ if (!entry) {
11
+ return cur
12
+ }
13
+
14
+ if (op.filter && !op.filter(entry)) {
15
+ return cur
16
+ }
17
+
18
+ if (!op.group) {
19
+ return cur
20
+ }
21
+ const groupName = op.group(entry, ext)
22
+
23
+ if (!groupName || !lUtils.isSafeKey(groupName)) {
24
+ return cur
25
+ }
26
+
27
+ if (!cur.res[groupName]) {
28
+ cur.res[groupName] = { acc: 0, cnt: 0, res: 0 }
29
+ }
30
+
31
+ const val = lUtils.getVal(entry, op.src)
32
+
33
+ cur.res[groupName].acc += val ? +val : 0
34
+ cur.res[groupName].cnt++
35
+
36
+ return cur
37
+ }
38
+
39
+ function tally (op, cur) {
40
+ for (const key in cur.res) {
41
+ if (cur.res[key].cnt) {
42
+ cur.res[key] = cur.res[key].acc / cur.res[key].cnt
43
+ } else {
44
+ cur.res[key] = null
45
+ }
46
+ }
47
+ return cur
48
+ }
49
+
50
+ module.exports = {
51
+ calc: groupAvg,
52
+ tally
53
+ }
@@ -0,0 +1,38 @@
1
+ 'use strict'
2
+
3
+ const lUtils = require('../utils')
4
+
5
+ function groupCnt (op, cur, entry, ext) {
6
+ if (!cur) {
7
+ cur = { res: lUtils.createResMap() }
8
+ }
9
+
10
+ if (!entry) {
11
+ return cur
12
+ }
13
+
14
+ if (op.filter && !op.filter(entry)) {
15
+ return cur
16
+ }
17
+
18
+ if (!op.group) {
19
+ return cur
20
+ }
21
+ const groupName = op.group(entry, ext)
22
+
23
+ if (!groupName || !lUtils.isSafeKey(groupName)) {
24
+ return cur
25
+ }
26
+
27
+ if (!cur.res[groupName]) {
28
+ cur.res[groupName] = 0
29
+ }
30
+
31
+ cur.res[groupName]++
32
+
33
+ return cur
34
+ }
35
+
36
+ module.exports = {
37
+ calc: groupCnt
38
+ }