@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,118 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Comments live on the persisted device record (`comments` array). The
5
+ * storage collaborators are injected — wire them to a
6
+ * DeviceProvisioningService (loadThing/saveThing/checkThingExists/generateId).
7
+ *
8
+ * Author identity: each method takes `(req, ctx)`. `ctx.user` is the
9
+ * authenticated identity supplied by the transport (gateway session /
10
+ * envelope requester) and always wins; a `req.user` that disagrees with it is
11
+ * rejected. Without `ctx.user` the service falls back to `req.user` — unless
12
+ * `requireAuthenticatedUser: true`, in which case the call is refused. Either
13
+ * way a user is mandatory, and only the comment's author may edit or delete
14
+ * it (comments stored without an author are not editable by anyone).
15
+ */
16
+ class CommentsService {
17
+ constructor ({ loadThing, saveThing, checkThingExists, generateId, requireAuthenticatedUser = false }) {
18
+ this.loadThing = loadThing
19
+ this.saveThing = saveThing
20
+ this.checkThingExists = checkThingExists
21
+ this.generateId = generateId
22
+ this.requireAuthenticatedUser = requireAuthenticatedUser
23
+ }
24
+
25
+ _resolveUser (req, ctx = {}) {
26
+ const authUser = ctx && ctx.user
27
+ if (authUser !== undefined && authUser !== null) {
28
+ if (typeof authUser !== 'string' || !authUser) throw new Error('ERR_COMMENT_USER_INVALID')
29
+ if (req.user !== undefined && req.user !== null && req.user !== authUser) {
30
+ throw new Error('ERR_COMMENT_USER_MISMATCH')
31
+ }
32
+ return authUser
33
+ }
34
+ if (this.requireAuthenticatedUser) throw new Error('ERR_COMMENT_USER_UNAUTHENTICATED')
35
+ if (typeof req.user !== 'string' || !req.user) throw new Error('ERR_COMMENT_USER_REQUIRED')
36
+ return req.user
37
+ }
38
+
39
+ _findCommentIndex (thg, req) {
40
+ if (req.id) {
41
+ return thg.comments.findIndex(c => c.id === req.id)
42
+ }
43
+ if (req.ts) {
44
+ return thg.comments.findIndex(c => c.ts === req.ts)
45
+ }
46
+ return -1
47
+ }
48
+
49
+ _checkCommentPermission (thg, commentIndex, user) {
50
+ const owner = thg.comments[commentIndex].user
51
+ if (typeof owner !== 'string' || !owner || owner !== user) {
52
+ throw new Error('ERR_COMMENT_ACCESS_DENIED')
53
+ }
54
+ }
55
+
56
+ async saveThingComment (req, ctx) {
57
+ const user = this._resolveUser(req, ctx)
58
+ this.checkThingExists(req)
59
+
60
+ const thg = await this.loadThing(req)
61
+
62
+ if (!Array.isArray(thg.comments)) thg.comments = []
63
+
64
+ thg.comments.push({
65
+ id: this.generateId(),
66
+ ts: Date.now(),
67
+ comment: req.comment,
68
+ user,
69
+ ...(req.pos ? { pos: req.pos } : {})
70
+ })
71
+
72
+ await this.saveThing(thg)
73
+
74
+ return 1
75
+ }
76
+
77
+ async editThingComment (req, ctx) {
78
+ const user = this._resolveUser(req, ctx)
79
+ this.checkThingExists(req)
80
+
81
+ const thg = await this.loadThing(req)
82
+
83
+ if (!Array.isArray(thg.comments)) throw new Error('ERR_THING_COMMENTS_NOTFOUND')
84
+
85
+ const commentIndex = this._findCommentIndex(thg, req)
86
+ if (commentIndex === -1) throw new Error('ERR_THING_COMMENT_NOTFOUND')
87
+
88
+ this._checkCommentPermission(thg, commentIndex, user)
89
+
90
+ thg.comments[commentIndex].comment = req.comment
91
+
92
+ await this.saveThing(thg)
93
+
94
+ return 1
95
+ }
96
+
97
+ async deleteThingComment (req, ctx) {
98
+ const user = this._resolveUser(req, ctx)
99
+ this.checkThingExists(req)
100
+
101
+ const thg = await this.loadThing(req)
102
+
103
+ if (!Array.isArray(thg.comments)) throw new Error('ERR_THING_COMMENTS_NOTFOUND')
104
+
105
+ const commentIndex = this._findCommentIndex(thg, req)
106
+ if (commentIndex === -1) throw new Error('ERR_THING_COMMENT_NOTFOUND')
107
+
108
+ this._checkCommentPermission(thg, commentIndex, user)
109
+
110
+ thg.comments.splice(commentIndex, 1)
111
+
112
+ await this.saveThing(thg)
113
+
114
+ return 1
115
+ }
116
+ }
117
+
118
+ module.exports = CommentsService
@@ -0,0 +1,307 @@
1
+ 'use strict'
2
+
3
+ const utilsStore = require('@tetherto/hp-svc-facs-store/utils')
4
+ const gLibUtilBase = require('@bitfinex/lib-js-util-base')
5
+ const { getLogsCountForTimeRange, getLogMaxHeight, aggregateLogs, applyFilters, projection } = require('../utils')
6
+
7
+ // Hard ceiling on rows a single read may return; conf.logMaxLimit lowers or
8
+ // raises it per worker.
9
+ const DEFAULT_MAX_LOG_LIMIT = 10000
10
+ const LOG_KEY_RE = /^[A-Za-z0-9_-]{1,64}$/
11
+ const LOG_TAG_RE = /^[A-Za-z0-9_.:-]{1,160}$/
12
+ // Per-device snapshot log written by SnapsService (`thing-5m-<deviceId>`).
13
+ const DEVICE_LOG_KEY = 'thing-5m'
14
+
15
+ /**
16
+ * Reads time logs written by LogsService: tailLog(key, tag) walks rotated
17
+ * bees until the requested range/limit is filled; getHistoricalLogs serves
18
+ * the `alerts` / `info` history logs, resolving device metadata through the
19
+ * injected getDeviceInfo(deviceId).
20
+ *
21
+ * Every caller-supplied paging value is validated: `offset` is an integer in
22
+ * 0..logMaxHeight, `limit` an integer capped at conf.logMaxLimit (default
23
+ * 10000), `start`/`end` finite timestamps. tailLog only opens the per-device
24
+ * snapshot log (`thing-5m`, tag = the device id) and `stat-*` aggregate
25
+ * logs, plus any key listed in conf.tailLogKeys; when the request is scoped
26
+ * to a device (thingId) the tag must belong to that device.
27
+ */
28
+ class LogHistoryService {
29
+ constructor ({ logs, getDeviceInfo, statTimeframes, conf, tailLogHook = async () => {} }) {
30
+ if (!logs) throw new Error('ERR_LOG_HISTORY_LOGS_REQUIRED')
31
+ this.logs = logs
32
+ this.getDeviceInfo = getDeviceInfo || (() => undefined)
33
+ this.statTimeframes = statTimeframes || []
34
+ this.conf = conf || {}
35
+ this.tailLogHook = tailLogHook
36
+ }
37
+
38
+ _maxLimit () {
39
+ const n = this.conf.logMaxLimit
40
+ return Number.isInteger(n) && n > 0 ? n : DEFAULT_MAX_LOG_LIMIT
41
+ }
42
+
43
+ // Normalises the paging fields of a caller request; throws ERR_LOG_* on
44
+ // anything outside the accepted shape.
45
+ _sanitizeRange (req) {
46
+ const out = { ...req }
47
+ const maxHeight = getLogMaxHeight(this.conf.logKeepCount)
48
+ if (req.offset !== undefined && req.offset !== null) {
49
+ const offset = Number(req.offset)
50
+ if (!Number.isInteger(offset) || offset < 0 || offset > maxHeight) throw new Error('ERR_LOG_OFFSET_INVALID')
51
+ out.offset = offset
52
+ }
53
+ if (req.limit !== undefined && req.limit !== null) {
54
+ const limit = Number(req.limit)
55
+ if (!Number.isInteger(limit) || limit < 0) throw new Error('ERR_LOG_LIMIT_INVALID')
56
+ out.limit = Math.min(limit, this._maxLimit())
57
+ }
58
+ for (const k of ['start', 'end']) {
59
+ if (req[k] === undefined || req[k] === null) continue
60
+ const v = Number(req[k])
61
+ if (!Number.isFinite(v) || v < 0) throw new Error(`ERR_LOG_${k.toUpperCase()}_INVALID`)
62
+ out[k] = v
63
+ }
64
+ return out
65
+ }
66
+
67
+ async _getLogs (req, logKey, errorMsg, transformFn) {
68
+ const offset = req.offset || 0
69
+ const log = await this.logs.getBeeTimeLog(logKey, offset)
70
+
71
+ if (!log) {
72
+ throw new Error(errorMsg)
73
+ }
74
+
75
+ const res = await this._parseHistLog(log, {
76
+ reverse: true,
77
+ limit: req.limit,
78
+ start: req.start,
79
+ end: req.end
80
+ })
81
+
82
+ if (!Array.isArray(res)) {
83
+ throw new Error('ERR_HIST_LOG_NOTFOUND')
84
+ }
85
+
86
+ await this.logs.releaseBeeTimeLog(log)
87
+ return transformFn(res, req)
88
+ }
89
+
90
+ _pickDevice (deviceId) {
91
+ const dev = this.getDeviceInfo(deviceId)
92
+ return gLibUtilBase.pick(dev, ['id', 'info', 'tags', 'type', 'code'])
93
+ }
94
+
95
+ _transformAlerts (res, req) {
96
+ const flattenedAlerts = res.flatMap(log => Object.values(log)).slice(0, req.limit)
97
+ return flattenedAlerts.map(alert => {
98
+ const { thingId, ...rest } = alert
99
+ return { ...rest, thing: this._pickDevice(thingId) }
100
+ })
101
+ }
102
+
103
+ _transformInfoHistory (res, req) {
104
+ const flattenedHistory = res.flat(1).slice(0, req.limit)
105
+ const historyWithCurrentInfo = flattenedHistory.map(change => {
106
+ const { id, ...rest } = change
107
+ return { ...rest, thing: this._pickDevice(id) }
108
+ })
109
+
110
+ return gLibUtilBase.isEmpty(req.fields) ? historyWithCurrentInfo : projection(historyWithCurrentInfo, req.fields)
111
+ }
112
+
113
+ async getHistoricalLogs (req) {
114
+ req = this._sanitizeRange(req)
115
+ const logType = req.logType
116
+ if (!logType) {
117
+ throw new Error('ERR_INFO_HISTORY_LOG_TYPE_INVALID')
118
+ }
119
+ if (logType === 'alerts') {
120
+ return this._getLogs(
121
+ req,
122
+ 'thing-alerts',
123
+ 'ERR_LOG_NOTFOUND',
124
+ (res, r) => this._transformAlerts(res, r)
125
+ )
126
+ }
127
+ if (logType === 'info') {
128
+ const logs = await this._getLogs(
129
+ req,
130
+ 'thing-history-log',
131
+ 'ERR_INFO_HISTORY_LOG_NOTFOUND',
132
+ (res, r) => this._transformInfoHistory(res, r)
133
+ )
134
+ return applyFilters(
135
+ logs,
136
+ { ...req, offset: req.offset ?? 0, limit: req.limit ?? 100 },
137
+ true
138
+ )
139
+ }
140
+ }
141
+
142
+ async _getLogResponse (req, offset) {
143
+ const log = await this.logs.getBeeTimeLog(`${req.key}-${req.tag}`, offset)
144
+
145
+ if (!log) {
146
+ throw new Error('ERR_LOG_NOTFOUND')
147
+ }
148
+
149
+ const res = await this._parseHistLog(log, {
150
+ reverse: true,
151
+ limit: req.limit,
152
+ start: req.start,
153
+ end: req.end
154
+ })
155
+ await this.logs.releaseBeeTimeLog(log)
156
+ return res
157
+ }
158
+
159
+ _getDefaultTaillogLimit (req) {
160
+ const { limit, start, end } = req
161
+ // A pure time-range read still stops at the hard ceiling.
162
+ return limit || (start || end ? this._maxLimit() : 100)
163
+ }
164
+
165
+ // Which logs a tailLog request may open. Keys and tags are interpolated
166
+ // into a bee name, so they are charset-restricted first; then the key must
167
+ // be the per-device snapshot log or a stat log, and a device-scoped request
168
+ // (thingId) may only read that device's own logs.
169
+ _checkTailLogScope (req) {
170
+ const { key, tag, thingId } = req
171
+ if (typeof key !== 'string' || !LOG_KEY_RE.test(key)) throw new Error('ERR_LOG_KEY_INVALID')
172
+ if (typeof tag !== 'string' || !LOG_TAG_RE.test(tag)) throw new Error('ERR_LOG_TAG_INVALID')
173
+
174
+ const extraKeys = Array.isArray(this.conf.tailLogKeys) ? this.conf.tailLogKeys : []
175
+ const isDeviceLog = key === DEVICE_LOG_KEY
176
+ if (!isDeviceLog && !key.startsWith('stat-') && !extraKeys.includes(key)) {
177
+ throw new Error('ERR_LOG_KEY_INVALID')
178
+ }
179
+
180
+ if (isDeviceLog) {
181
+ // tag === thingId here when the request is device-scoped (tailLog
182
+ // derives it); otherwise it must still name a provisioned device.
183
+ if (!this.getDeviceInfo(tag)) throw new Error('ERR_LOG_TAG_FORBIDDEN')
184
+ return
185
+ }
186
+
187
+ if (thingId) {
188
+ const dev = this.getDeviceInfo(thingId)
189
+ const tags = (dev && Array.isArray(dev.tags)) ? dev.tags : []
190
+ if (!tags.includes(tag)) throw new Error('ERR_LOG_TAG_FORBIDDEN')
191
+ }
192
+ }
193
+
194
+ async _getTailLogWithOffset (req, offset) {
195
+ const { start, end, key } = req
196
+ const limit = this._getDefaultTaillogLimit(req)
197
+ const numberOfLogsInRange = getLogsCountForTimeRange(
198
+ start,
199
+ end,
200
+ key,
201
+ this.statTimeframes
202
+ )
203
+ const logMaxHeight = getLogMaxHeight(this.conf.logKeepCount)
204
+
205
+ if (!numberOfLogsInRange && !limit) return await this._getLogResponse(req, offset)
206
+
207
+ const remaining =
208
+ numberOfLogsInRange && limit
209
+ ? Math.min(numberOfLogsInRange, limit)
210
+ : numberOfLogsInRange || limit
211
+
212
+ let allLogs = []
213
+ if (offset >= logMaxHeight) return await this._getLogResponse(req, offset)
214
+ while (allLogs.length < remaining && offset <= logMaxHeight) {
215
+ try {
216
+ const logs = await this._getLogResponse(req, offset++)
217
+ if (!Array.isArray(logs)) break
218
+
219
+ allLogs = allLogs.concat(logs).slice(0, remaining)
220
+ } catch (error) {
221
+ if (allLogs.length === 0) throw error
222
+ break
223
+ }
224
+ }
225
+
226
+ return allLogs
227
+ }
228
+
229
+ async tailLog (req) {
230
+ if (!req.key) {
231
+ throw new Error('ERR_LOG_KEY_NOTFOUND')
232
+ }
233
+
234
+ // The per-device snapshot log is addressed by the device, never by a
235
+ // caller-chosen tag.
236
+ if (req.key === DEVICE_LOG_KEY && req.thingId) {
237
+ req = { ...req, tag: req.thingId }
238
+ }
239
+
240
+ if (!req.tag) {
241
+ throw new Error('ERR_LOG_TAG_INVALID')
242
+ }
243
+
244
+ this._checkTailLogScope(req)
245
+ req = this._sanitizeRange(req)
246
+ const offset = req.offset || 0
247
+
248
+ const res = await this._getTailLogWithOffset(req, offset)
249
+
250
+ await this.tailLogHook(res, req)
251
+
252
+ if (req.groupRange) {
253
+ const aggregated = aggregateLogs(res, req.groupRange, req.shouldCalculateAvg)
254
+
255
+ if (!gLibUtilBase.isEmpty(req.fields)) {
256
+ return projection(aggregated, req.fields)
257
+ }
258
+ return aggregated
259
+ }
260
+
261
+ if (!gLibUtilBase.isEmpty(req.fields)) {
262
+ return projection(res, req.fields)
263
+ }
264
+
265
+ return res
266
+ }
267
+
268
+ async _parseHistLog (log, req) {
269
+ const query = {}
270
+
271
+ if (req.start) {
272
+ query.gte = utilsStore.convIntToBin(req.start)
273
+ }
274
+
275
+ if (req.end) {
276
+ query.lte = utilsStore.convIntToBin(req.end)
277
+ }
278
+
279
+ if (req.limit) {
280
+ query.limit = req.limit
281
+ }
282
+
283
+ if (req.reverse) {
284
+ query.reverse = true
285
+ }
286
+
287
+ if (req.startExcl) {
288
+ query.gt = utilsStore.convIntToBin(req.startExcl)
289
+ }
290
+
291
+ if (req.endExcl) {
292
+ query.lt = utilsStore.convIntToBin(req.endExcl)
293
+ }
294
+
295
+ const stream = log.createReadStream(query)
296
+
297
+ const res = []
298
+
299
+ for await (const chunk of stream) {
300
+ res.push(JSON.parse(chunk.value.toString()))
301
+ }
302
+
303
+ return res
304
+ }
305
+ }
306
+
307
+ module.exports = LogHistoryService
@@ -0,0 +1,249 @@
1
+ 'use strict'
2
+
3
+ const debug = require('debug')('mdk:logs')
4
+ const utilsStore = require('@tetherto/hp-svc-facs-store/utils')
5
+ const { getLogMaxHeight } = require('../utils')
6
+
7
+ function getLogName (n) {
8
+ return n + '-5'
9
+ }
10
+
11
+ /**
12
+ * Bee-backed time logs. One meta entry per log key tracks the current
13
+ * rotation point (`cur`); the physical bee for (key, point) is
14
+ * `<key>-5-<point>`. All values are JSON, keys are binary-encoded ints (ts).
15
+ */
16
+ class LogsService {
17
+ constructor ({ store, metaLogs, conf }) {
18
+ if (!store) throw new Error('ERR_LOGS_STORE_REQUIRED')
19
+ if (!metaLogs) throw new Error('ERR_LOGS_META_LOGS_REQUIRED')
20
+ this.store = store
21
+ this.metaLogs = metaLogs
22
+ this.conf = conf || {}
23
+ this._logCache = {}
24
+ }
25
+
26
+ async initBeeLogMeta (logKey) {
27
+ const meta = { cur: 0 }
28
+
29
+ await this.metaLogs.put(
30
+ logKey,
31
+ Buffer.from(JSON.stringify(meta))
32
+ )
33
+
34
+ return meta
35
+ }
36
+
37
+ async rotateBeeLog (logKey) {
38
+ const meta = await this.getBeeLogMeta(logKey)
39
+
40
+ if (!meta) {
41
+ throw new Error('ERR_BEE_LOG_META_NOTFOUND')
42
+ }
43
+
44
+ meta.cur++
45
+
46
+ await this.metaLogs.put(
47
+ logKey,
48
+ Buffer.from(JSON.stringify(meta))
49
+ )
50
+
51
+ return meta
52
+ }
53
+
54
+ async getBeeLogMeta (logKey, init = false) {
55
+ let meta = await this.metaLogs.get(logKey)
56
+
57
+ if (meta) {
58
+ meta = JSON.parse(meta.value.toString())
59
+ } else if (init) {
60
+ meta = await this.initBeeLogMeta(logKey)
61
+ }
62
+
63
+ return meta
64
+ }
65
+
66
+ async releaseBeeTimeLog (log) {
67
+ try {
68
+ await log.close()
69
+ } catch (e) {
70
+ debug('ERR_RELEASE_BEE %s', e.message)
71
+ }
72
+ }
73
+
74
+ // `offset` counts rotations back from the current bee. It reaches the
75
+ // store as part of a bee name, and store.getBee() creates whatever it is
76
+ // asked for — so a negative, fractional or out-of-window offset would mint
77
+ // a fresh hypercore per call. Only integers in 0..logMaxHeight are valid.
78
+ _checkOffset (offset) {
79
+ if (!Number.isInteger(offset) || offset < 0) throw new Error('ERR_LOG_OFFSET_INVALID')
80
+ return offset <= getLogMaxHeight(this.conf.logKeepCount)
81
+ }
82
+
83
+ async getBeeTimeLog (logKey, offset = 0, init = false) {
84
+ if (!this._checkOffset(offset)) return null
85
+
86
+ const meta = await this.getBeeLogMeta(logKey, init)
87
+
88
+ if (!meta) {
89
+ return null
90
+ }
91
+
92
+ const point = meta.cur - offset
93
+
94
+ if (point < 0) {
95
+ return null
96
+ }
97
+
98
+ let log = null
99
+
100
+ try {
101
+ log = await this.getBeeForLogKey(logKey, point)
102
+
103
+ if (offset === 0 && log && (await this.isLogCorrupted(log))) {
104
+ const updatedMeta = await this.rotateBeeLog(logKey)
105
+ if (!updatedMeta) {
106
+ throw new Error('ERR_BEE_ROTATED_LOG_META_NOTFOUND')
107
+ }
108
+ log = await this.getBeeForLogKey(logKey, updatedMeta.cur)
109
+ }
110
+
111
+ if (!log) {
112
+ return null
113
+ }
114
+ } catch (error) {
115
+ debug('ERR_GET_BEE_TIME_LOG %s', error.message)
116
+ return null
117
+ }
118
+
119
+ try {
120
+ await log.ready()
121
+ } catch (e) {
122
+ debug('ERR_BEE_READY %s', e.message)
123
+ log = null
124
+ }
125
+
126
+ return log
127
+ }
128
+
129
+ async getBeeForLogKey (logKey, point) {
130
+ return await this.store.getBee(
131
+ {
132
+ name: `${getLogName(logKey)}-${point}`
133
+ },
134
+ { keyEncoding: 'binary' }
135
+ )
136
+ }
137
+
138
+ async isLogCorrupted (log) {
139
+ let validated = true
140
+ try {
141
+ for await (const entry of log.createReadStream({ limit: 1 })) {
142
+ validated = !!entry
143
+ }
144
+ } catch (e) {
145
+ validated = false
146
+ debug('LOG_CORRUPTED %s', e.message)
147
+ }
148
+
149
+ return !validated
150
+ }
151
+
152
+ async rotateLogs () {
153
+ if (!this.conf.logRotateMaxLength) {
154
+ return []
155
+ }
156
+
157
+ const stream = this.metaLogs.createReadStream({})
158
+ const res = []
159
+
160
+ for await (const chunk of stream) {
161
+ const meta = JSON.parse(chunk.value.toString())
162
+ const log = await this.getBeeTimeLog(chunk.key, 0)
163
+
164
+ if (log) {
165
+ if (log.core.length >= this.conf.logRotateMaxLength) {
166
+ await this.rotateBeeLog(chunk.key)
167
+ res.push([chunk.key, meta, log.core.length])
168
+ debug('ROTATE: log-key=%s,cur=%d,len=%d', chunk.key, meta.cur, log.core.length)
169
+ }
170
+
171
+ await this.releaseBeeTimeLog(log)
172
+ }
173
+ }
174
+
175
+ return res
176
+ }
177
+
178
+ async refreshLogsCache () {
179
+ if (!this.conf.logKeepCount) {
180
+ return []
181
+ }
182
+
183
+ const stream = this.metaLogs.createReadStream({})
184
+ const mem = this._logCache
185
+
186
+ for (const ck of Object.keys(mem)) {
187
+ mem[ck].offset = -1
188
+ }
189
+
190
+ for await (const chunk of stream) {
191
+ const meta = JSON.parse(chunk.value.toString())
192
+ const maxHeight = getLogMaxHeight(this.conf.logKeepCount)
193
+
194
+ for (let i = 0; i < maxHeight; i++) {
195
+ const log = await this.getBeeTimeLog(chunk.key, i)
196
+
197
+ if (!log) {
198
+ continue
199
+ }
200
+
201
+ const dkey = log.core.key.toString('hex')
202
+
203
+ if (mem[dkey]) {
204
+ mem[dkey].offset = i
205
+ await this.releaseBeeTimeLog(log)
206
+ continue
207
+ }
208
+
209
+ debug('REFRESH: log-key=%s,offset=%d,cur=%d', chunk.key, i, meta.cur)
210
+ mem[dkey] = { log, offset: i, desc: chunk.key }
211
+ }
212
+ }
213
+
214
+ await this._cleanupLogs(mem)
215
+ }
216
+
217
+ async _cleanupLogs (mem) {
218
+ for (const ck of Object.keys(mem)) {
219
+ const entry = mem[ck]
220
+
221
+ if (entry.offset < 0) {
222
+ await this.releaseBeeTimeLog(entry.log)
223
+ debug('REFRESH[RELEASE]: log-key=%s,cache-key=%s', entry.desc, ck)
224
+ delete mem[ck]
225
+
226
+ try {
227
+ await this.store.unlink(ck)
228
+ break
229
+ } catch (e) {
230
+ debug('LOG_NOT_FOUND=%s %s', ck, e.message)
231
+ }
232
+ }
233
+ }
234
+ }
235
+
236
+ async saveLogData (key, ts, data, offset = 0, init = false) {
237
+ const log = await this.getBeeTimeLog(key, offset, init)
238
+ if (!log) return
239
+
240
+ await log.put(utilsStore.convIntToBin(ts), Buffer.from(JSON.stringify(data)))
241
+ await this.releaseBeeTimeLog(log)
242
+ }
243
+
244
+ getLogName (n) {
245
+ return getLogName(n)
246
+ }
247
+ }
248
+
249
+ module.exports = LogsService
@@ -0,0 +1,20 @@
1
+ 'use strict'
2
+
3
+ const SCHEDULER_TIMES = {
4
+ _1M: { time: '0 */1 * * * *', key: '1m' },
5
+ _5M: { time: '0 */5 * * * *', key: '5m' },
6
+ _1D: { time: '0 0 0 * * *', key: '1D' }
7
+ }
8
+
9
+ const BTC_SATS = 100000000 // 1 BTC = 100,000,000 satoshis
10
+ const MINUTE_MS = 60 * 1000
11
+ const HOUR_MS = 60 * 60 * 1000
12
+ const HOURS_24_MS = 24 * 60 * 60 * 1000
13
+
14
+ module.exports = {
15
+ SCHEDULER_TIMES,
16
+ BTC_SATS,
17
+ MINUTE_MS,
18
+ HOUR_MS,
19
+ HOURS_24_MS
20
+ }