@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.
- package/LICENSE +202 -0
- package/README.md +572 -0
- package/index.js +519 -0
- package/lib/address-policy.js +124 -0
- package/lib/local-discovery.js +321 -0
- package/lib/services/actions.service.js +68 -0
- package/lib/services/alerts.service.js +207 -0
- package/lib/services/comments.service.js +118 -0
- package/lib/services/log-history.service.js +307 -0
- package/lib/services/logs.service.js +249 -0
- package/lib/services/pool-utils/constants.js +20 -0
- package/lib/services/pool-utils/time.js +49 -0
- package/lib/services/pool.service.js +397 -0
- package/lib/services/provisioning.service.js +420 -0
- package/lib/services/settings.service.js +26 -0
- package/lib/services/snaps.service.js +181 -0
- package/lib/services/stats.service.js +205 -0
- package/lib/stats/README.md +80 -0
- package/lib/stats/index.js +57 -0
- package/lib/stats/ops/alerts_aggr.js +40 -0
- package/lib/stats/ops/alerts_group_cnt.js +43 -0
- package/lib/stats/ops/arr_concat.js +27 -0
- package/lib/stats/ops/array_obj_calc.js +41 -0
- package/lib/stats/ops/avg.js +42 -0
- package/lib/stats/ops/cnt.js +19 -0
- package/lib/stats/ops/group.js +38 -0
- package/lib/stats/ops/group_avg.js +53 -0
- package/lib/stats/ops/group_cnt.js +38 -0
- package/lib/stats/ops/group_max.js +40 -0
- package/lib/stats/ops/group_multiple_stats.js +40 -0
- package/lib/stats/ops/group_sum.js +40 -0
- package/lib/stats/ops/index.js +35 -0
- package/lib/stats/ops/nested_obj_concat.js +54 -0
- package/lib/stats/ops/obj_concat.js +33 -0
- package/lib/stats/ops/sum.js +27 -0
- package/lib/stats/utils.js +99 -0
- package/lib/templates/alerts.js +128 -0
- package/lib/templates/stats.js +476 -0
- package/lib/things/constants.js +65 -0
- package/lib/things/container.js +84 -0
- package/lib/things/miner.js +190 -0
- package/lib/things/powermeter.js +30 -0
- package/lib/things/sensor.js +18 -0
- package/lib/things/thing.js +93 -0
- package/lib/utils.js +603 -0
- package/lib/worker-infra.js +221 -0
- package/package.json +64 -1
- package/services.js +246 -0
- package/utils/compose-yaml.js +56 -0
- package/utils/constants.js +30 -0
- package/utils/index.js +29 -0
- package/utils/initialize.js +148 -0
- package/utils/service-bootstrap.js +271 -0
- 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
|
+
}
|