@powerhousedao/analytics-engine-core 6.0.0-dev.105 → 6.0.0-dev.107
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/dist/index.d.ts +130 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +672 -0
- package/dist/index.js.map +1 -0
- package/package.json +9 -7
- package/dist/src/AnalyticsDiscretizer.d.ts +0 -60
- package/dist/src/AnalyticsDiscretizer.d.ts.map +0 -1
- package/dist/src/AnalyticsPath.d.ts +0 -2
- package/dist/src/AnalyticsPath.d.ts.map +0 -1
- package/dist/src/AnalyticsPeriod.d.ts +0 -2
- package/dist/src/AnalyticsPeriod.d.ts.map +0 -1
- package/dist/src/AnalyticsProfiler.d.ts +0 -31
- package/dist/src/AnalyticsProfiler.d.ts.map +0 -1
- package/dist/src/AnalyticsQuery.d.ts +0 -3
- package/dist/src/AnalyticsQuery.d.ts.map +0 -1
- package/dist/src/AnalyticsQueryEngine.d.ts +0 -20
- package/dist/src/AnalyticsQueryEngine.d.ts.map +0 -1
- package/dist/src/AnalyticsQueryResult.d.ts +0 -10
- package/dist/src/AnalyticsQueryResult.d.ts.map +0 -1
- package/dist/src/AnalyticsSubscriptionManager.d.ts +0 -48
- package/dist/src/AnalyticsSubscriptionManager.d.ts.map +0 -1
- package/dist/src/AnalyticsTimeSlicer.d.ts +0 -29
- package/dist/src/AnalyticsTimeSlicer.d.ts.map +0 -1
- package/dist/src/IAnalyticsCache.d.ts +0 -7
- package/dist/src/IAnalyticsCache.d.ts.map +0 -1
- package/dist/src/IAnalyticsStore.d.ts +0 -2
- package/dist/src/IAnalyticsStore.d.ts.map +0 -1
- package/dist/src/index.d.ts +0 -11
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js +0 -5576
package/dist/index.js
ADDED
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
import { AnalyticsGranularity, AnalyticsPath, AnalyticsPathSegment, AnalyticsPeriod, AnalyticsSerializerTypes, CompoundOperator } from "@powerhousedao/shared/analytics";
|
|
2
|
+
import { DateTime, Interval } from "luxon";
|
|
3
|
+
//#region src/AnalyticsProfiler.ts
|
|
4
|
+
var AnalyticsProfiler = class {
|
|
5
|
+
_stack = [];
|
|
6
|
+
_prefix = "";
|
|
7
|
+
constructor(_ns, _logger) {
|
|
8
|
+
this._ns = _ns;
|
|
9
|
+
this._logger = _logger;
|
|
10
|
+
this._prefix = _ns;
|
|
11
|
+
}
|
|
12
|
+
get prefix() {
|
|
13
|
+
return this._prefix;
|
|
14
|
+
}
|
|
15
|
+
push(system) {
|
|
16
|
+
this._stack.push(system);
|
|
17
|
+
this.updatePrefix();
|
|
18
|
+
}
|
|
19
|
+
pop() {
|
|
20
|
+
if (this._stack.pop()) this.updatePrefix();
|
|
21
|
+
}
|
|
22
|
+
popAndReturn(result) {
|
|
23
|
+
this.pop();
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
async record(metric, fn) {
|
|
27
|
+
const start = performance.now();
|
|
28
|
+
const fullMetric = `${this.prefix}.${metric}`;
|
|
29
|
+
try {
|
|
30
|
+
return await fn();
|
|
31
|
+
} finally {
|
|
32
|
+
this._logger(fullMetric, performance.now() - start);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
recordSync(metric, fn) {
|
|
36
|
+
const start = performance.now();
|
|
37
|
+
const fullMetric = `${this.prefix}.${metric}`;
|
|
38
|
+
try {
|
|
39
|
+
return fn();
|
|
40
|
+
} finally {
|
|
41
|
+
this._logger(fullMetric, performance.now() - start);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
updatePrefix() {
|
|
45
|
+
if (this._stack.length > 0) this._prefix = `${this._ns}.${this._stack.join(".")}`;
|
|
46
|
+
else this._prefix = this._ns;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
var PassthroughAnalyticsProfiler = class {
|
|
50
|
+
get prefix() {
|
|
51
|
+
return "";
|
|
52
|
+
}
|
|
53
|
+
push(system) {}
|
|
54
|
+
pop() {}
|
|
55
|
+
popAndReturn(result) {
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
async record(metric, fn) {
|
|
59
|
+
return await fn();
|
|
60
|
+
}
|
|
61
|
+
recordSync(metric, fn) {
|
|
62
|
+
return fn();
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region src/AnalyticsTimeSlicer.ts
|
|
67
|
+
const getPeriodSeriesArray = (range) => {
|
|
68
|
+
const result = [];
|
|
69
|
+
const series = getPeriodSeries(range);
|
|
70
|
+
let next = series.next();
|
|
71
|
+
while (next) {
|
|
72
|
+
result.push(next);
|
|
73
|
+
next = series.next();
|
|
74
|
+
}
|
|
75
|
+
return result;
|
|
76
|
+
};
|
|
77
|
+
const getPeriodSeries = (range) => {
|
|
78
|
+
return {
|
|
79
|
+
...range,
|
|
80
|
+
next: _createFactoryFn(range)
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
const _createFactoryFn = (range) => {
|
|
84
|
+
let current = range.start;
|
|
85
|
+
return () => {
|
|
86
|
+
if (current == null) return null;
|
|
87
|
+
let result = null;
|
|
88
|
+
switch (range.granularity) {
|
|
89
|
+
case AnalyticsGranularity.Total:
|
|
90
|
+
result = _nextTotalPeriod(current, range.end);
|
|
91
|
+
break;
|
|
92
|
+
case AnalyticsGranularity.Annual:
|
|
93
|
+
result = _nextAnnualPeriod(current, range.end);
|
|
94
|
+
break;
|
|
95
|
+
case AnalyticsGranularity.SemiAnnual:
|
|
96
|
+
result = _nextSemiAnnualPeriod(current, range.end);
|
|
97
|
+
break;
|
|
98
|
+
case AnalyticsGranularity.Quarterly:
|
|
99
|
+
result = _nextQuarterlyPeriod(current, range.end);
|
|
100
|
+
break;
|
|
101
|
+
case AnalyticsGranularity.Monthly:
|
|
102
|
+
result = _nextMonthlyPeriod(current, range.end);
|
|
103
|
+
break;
|
|
104
|
+
case AnalyticsGranularity.Weekly:
|
|
105
|
+
result = _nextWeeklyPeriod(current, range.end);
|
|
106
|
+
break;
|
|
107
|
+
case AnalyticsGranularity.Daily:
|
|
108
|
+
result = _nextDailyPeriod(current, range.end);
|
|
109
|
+
break;
|
|
110
|
+
case AnalyticsGranularity.Hourly: result = _nextHourlyPeriod(current, range.end);
|
|
111
|
+
}
|
|
112
|
+
if (result === null) current = null;
|
|
113
|
+
else current = result.end.plus({ milliseconds: 1 });
|
|
114
|
+
return result;
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
const _nextTotalPeriod = (nextStart, seriesEnd) => {
|
|
118
|
+
if (seriesEnd <= nextStart) return null;
|
|
119
|
+
return {
|
|
120
|
+
period: "total",
|
|
121
|
+
start: nextStart,
|
|
122
|
+
end: seriesEnd
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
const _nextAnnualPeriod = (nextStart, seriesEnd) => {
|
|
126
|
+
if (seriesEnd <= nextStart) return null;
|
|
127
|
+
const inputUtc = nextStart.toUTC();
|
|
128
|
+
return {
|
|
129
|
+
period: "annual",
|
|
130
|
+
start: nextStart,
|
|
131
|
+
end: DateTime.utc(inputUtc.year, inputUtc.month, inputUtc.day).plus({ years: 1 })
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
const _nextSemiAnnualPeriod = (nextStart, seriesEnd) => {
|
|
135
|
+
if (seriesEnd <= nextStart) return null;
|
|
136
|
+
const midYear = DateTime.utc(nextStart.year, 7, 1);
|
|
137
|
+
const endYear = DateTime.utc(nextStart.year, 12, 31, 23, 59, 59, 999);
|
|
138
|
+
let endDate;
|
|
139
|
+
if (midYear > nextStart) endDate = midYear;
|
|
140
|
+
else endDate = endYear;
|
|
141
|
+
if (endDate > seriesEnd) endDate = seriesEnd;
|
|
142
|
+
return {
|
|
143
|
+
period: "semiAnnual",
|
|
144
|
+
start: nextStart,
|
|
145
|
+
end: endDate
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
const _nextQuarterlyPeriod = (nextStart, seriesEnd) => {
|
|
149
|
+
if (seriesEnd <= nextStart) return null;
|
|
150
|
+
let endDate;
|
|
151
|
+
const nextStartUtc = nextStart.toUTC();
|
|
152
|
+
const startMonth = nextStartUtc.month;
|
|
153
|
+
if (startMonth < 3) endDate = DateTime.utc(nextStartUtc.year, 4, 1);
|
|
154
|
+
else if (startMonth < 6) endDate = DateTime.utc(nextStartUtc.year, 7, 1);
|
|
155
|
+
else if (startMonth < 9) endDate = DateTime.utc(nextStartUtc.year, 10, 1);
|
|
156
|
+
else endDate = DateTime.utc(nextStartUtc.year, 12, 31, 23, 59, 59, 999);
|
|
157
|
+
if (endDate > seriesEnd) endDate = seriesEnd;
|
|
158
|
+
return {
|
|
159
|
+
period: "quarterly",
|
|
160
|
+
start: nextStart,
|
|
161
|
+
end: endDate
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
const _nextMonthlyPeriod = (nextStart, seriesEnd) => {
|
|
165
|
+
if (seriesEnd <= nextStart) return null;
|
|
166
|
+
const nextStartUtc = nextStart.toUTC();
|
|
167
|
+
let endDate = DateTime.utc(nextStartUtc.year, nextStartUtc.month, nextStartUtc.day).plus({ months: 1 }).startOf("month");
|
|
168
|
+
if (endDate > seriesEnd) {
|
|
169
|
+
if (!nextStart.hasSame(seriesEnd, "month")) endDate = seriesEnd;
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
period: "monthly",
|
|
173
|
+
start: nextStart,
|
|
174
|
+
end: endDate
|
|
175
|
+
};
|
|
176
|
+
};
|
|
177
|
+
const _nextWeeklyPeriod = (nextStart, seriesEnd) => {
|
|
178
|
+
if (seriesEnd <= nextStart) return null;
|
|
179
|
+
const nextStartUtc = nextStart.toUTC();
|
|
180
|
+
const nextWeekStartUTC = DateTime.utc(nextStartUtc.year, nextStartUtc.month, nextStartUtc.day).plus({ weeks: 1 }).startOf("week");
|
|
181
|
+
if (nextWeekStartUTC > seriesEnd) {
|
|
182
|
+
if (!nextWeekStartUTC.hasSame(seriesEnd, "day")) return {
|
|
183
|
+
period: "weekly",
|
|
184
|
+
start: nextStart,
|
|
185
|
+
end: seriesEnd
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
period: "weekly",
|
|
190
|
+
start: nextStart,
|
|
191
|
+
end: nextWeekStartUTC
|
|
192
|
+
};
|
|
193
|
+
};
|
|
194
|
+
const _nextDailyPeriod = (nextStart, seriesEnd) => {
|
|
195
|
+
if (seriesEnd <= nextStart) return null;
|
|
196
|
+
let endDate = nextStart.toUTC().plus({ days: 1 }).startOf("day");
|
|
197
|
+
if (endDate > seriesEnd || endDate.hasSame(seriesEnd, "day")) endDate = seriesEnd;
|
|
198
|
+
return {
|
|
199
|
+
period: "daily",
|
|
200
|
+
start: nextStart,
|
|
201
|
+
end: endDate
|
|
202
|
+
};
|
|
203
|
+
};
|
|
204
|
+
const _nextHourlyPeriod = (nextStart, seriesEnd) => {
|
|
205
|
+
if (seriesEnd <= nextStart) return null;
|
|
206
|
+
let endDate = nextStart.toUTC().plus({ hours: 1 });
|
|
207
|
+
if (endDate > seriesEnd) {
|
|
208
|
+
if (nextStart.hour !== seriesEnd.hour) endDate = seriesEnd.toUTC();
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
period: "hourly",
|
|
212
|
+
start: nextStart,
|
|
213
|
+
end: endDate
|
|
214
|
+
};
|
|
215
|
+
};
|
|
216
|
+
//#endregion
|
|
217
|
+
//#region src/AnalyticsDiscretizer.ts
|
|
218
|
+
const getQuarter = (date) => {
|
|
219
|
+
return Math.floor((date.month - 1) / 3) + 1;
|
|
220
|
+
};
|
|
221
|
+
var AnalyticsDiscretizer = class AnalyticsDiscretizer {
|
|
222
|
+
static discretize(series, dimensions, start, end, granularity) {
|
|
223
|
+
const index = this._buildIndex(series, dimensions);
|
|
224
|
+
const periods = getPeriodSeriesArray(this._calculateRange(start, end, granularity, series));
|
|
225
|
+
const disretizedResults = this._discretizeNode(index, {}, dimensions, periods);
|
|
226
|
+
return this._groupResultsByPeriod(periods, disretizedResults);
|
|
227
|
+
}
|
|
228
|
+
static _calculateRange(start, end, granularity, results) {
|
|
229
|
+
let calculatedStart = start || null;
|
|
230
|
+
let calculatedEnd = end || null;
|
|
231
|
+
if (calculatedStart == null || calculatedEnd == null) for (const r of results) {
|
|
232
|
+
if (calculatedStart == null) calculatedStart = r.start;
|
|
233
|
+
const endValue = r.end || r.start;
|
|
234
|
+
if (calculatedEnd == null || calculatedEnd < endValue) calculatedEnd = endValue;
|
|
235
|
+
}
|
|
236
|
+
if (calculatedStart == null || calculatedEnd == null) throw new Error("Cannot determine query start and/or end.");
|
|
237
|
+
return {
|
|
238
|
+
start: calculatedStart,
|
|
239
|
+
end: calculatedEnd,
|
|
240
|
+
granularity
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
static _groupResultsByPeriod(periods, dimensionedResults) {
|
|
244
|
+
const result = {};
|
|
245
|
+
for (const p of periods) {
|
|
246
|
+
const id = p.start.toISO() + "-" + p.period;
|
|
247
|
+
result[id] = {
|
|
248
|
+
period: AnalyticsDiscretizer._getPeriodString(p),
|
|
249
|
+
start: p.start,
|
|
250
|
+
end: p.end,
|
|
251
|
+
rows: []
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
for (const r of dimensionedResults) for (const period of Object.keys(r.series)) result[period].rows.push({
|
|
255
|
+
dimensions: r.dimensions,
|
|
256
|
+
metric: r.metric,
|
|
257
|
+
unit: r.unit == "__NULL__" ? null : r.unit,
|
|
258
|
+
value: r.series[period].inc,
|
|
259
|
+
sum: r.series[period].sum
|
|
260
|
+
});
|
|
261
|
+
return Object.values(result);
|
|
262
|
+
}
|
|
263
|
+
static _getPeriodString(p) {
|
|
264
|
+
switch (p.period) {
|
|
265
|
+
case "annual": return p.start.year.toString();
|
|
266
|
+
case "semiAnnual": return `${p.start.year}/${p.start.month < 7 ? "H1" : "H2"}`;
|
|
267
|
+
case "quarterly": return `${p.start.year}/Q${getQuarter(p.start)}`;
|
|
268
|
+
case "monthly":
|
|
269
|
+
const month = p.start.toUTC().month;
|
|
270
|
+
const formattedMonth = month < 10 ? `0${month}` : `${month}`;
|
|
271
|
+
return `${p.start.year}/${formattedMonth}`;
|
|
272
|
+
case "weekly": return `${p.start.weekYear}/W${p.start.weekNumber}`;
|
|
273
|
+
case "daily":
|
|
274
|
+
const monthD = p.start.month;
|
|
275
|
+
const day = p.start.day;
|
|
276
|
+
const formattedMonthD = monthD < 10 ? `0${monthD}` : `${monthD}`;
|
|
277
|
+
const formattedDay = day < 10 ? `0${day}` : `${day}`;
|
|
278
|
+
return `${p.start.year}/${formattedMonthD}/${formattedDay}`;
|
|
279
|
+
case "hourly":
|
|
280
|
+
const monthH = p.start.month;
|
|
281
|
+
const dayH = p.start.day;
|
|
282
|
+
const hourH = p.start.hour;
|
|
283
|
+
const formattedMonthH = monthH < 10 ? `0${monthH}` : `${monthH}`;
|
|
284
|
+
const formattedDayH = dayH < 10 ? `0${dayH}` : `${dayH}`;
|
|
285
|
+
const formattedHourH = hourH < 10 ? `0${hourH}` : `${hourH}`;
|
|
286
|
+
return `${p.start.year}/${formattedMonthH}/${formattedDayH}/${formattedHourH}`;
|
|
287
|
+
default: return p.period;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
static _discretizeNode(node, dimensionValues, remainingDimensions, periods) {
|
|
291
|
+
const result = [];
|
|
292
|
+
if (remainingDimensions.length > 0) {
|
|
293
|
+
const subdimension = remainingDimensions[0];
|
|
294
|
+
Object.keys(node).forEach((subdimensionValue, index, arr) => {
|
|
295
|
+
const newDimensionValues = { ...dimensionValues };
|
|
296
|
+
newDimensionValues[subdimension] = subdimensionValue;
|
|
297
|
+
result.push(...this._discretizeNode(node[subdimensionValue], newDimensionValues, remainingDimensions.slice(1), periods));
|
|
298
|
+
});
|
|
299
|
+
} else Object.keys(node).forEach((metric) => {
|
|
300
|
+
result.push(...this._discretizeLeaf(node[metric], periods, metric, dimensionValues));
|
|
301
|
+
});
|
|
302
|
+
return result;
|
|
303
|
+
}
|
|
304
|
+
static _discretizeLeaf(leaf, periods, metric, dimensionValues) {
|
|
305
|
+
const result = [];
|
|
306
|
+
Object.keys(leaf).forEach((unit) => {
|
|
307
|
+
const metaDimensions = {};
|
|
308
|
+
Object.keys(dimensionValues).forEach((k) => {
|
|
309
|
+
metaDimensions[k] = {
|
|
310
|
+
path: leaf[unit][0].dimensions[k],
|
|
311
|
+
icon: leaf[unit][0].dimensions.icon,
|
|
312
|
+
label: leaf[unit][0].dimensions.label,
|
|
313
|
+
description: leaf[unit][0].dimensions.description
|
|
314
|
+
};
|
|
315
|
+
});
|
|
316
|
+
result.push({
|
|
317
|
+
unit,
|
|
318
|
+
metric,
|
|
319
|
+
dimensions: metaDimensions,
|
|
320
|
+
series: this._discretizeSeries(leaf[unit], periods)
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
return result;
|
|
324
|
+
}
|
|
325
|
+
static _discretizeSeries(series, periods) {
|
|
326
|
+
const result = {};
|
|
327
|
+
for (const s of series) {
|
|
328
|
+
let oldSum = this._getValue(s, periods[0].start);
|
|
329
|
+
for (const p of periods) {
|
|
330
|
+
const newSum = this._getValue(s, p.end);
|
|
331
|
+
const id = `${p.start.toISO()}-${p.period}`;
|
|
332
|
+
if (result[id]) {
|
|
333
|
+
result[id].inc += newSum - oldSum;
|
|
334
|
+
result[id].sum += newSum;
|
|
335
|
+
} else result[id] = {
|
|
336
|
+
inc: newSum - oldSum,
|
|
337
|
+
sum: newSum
|
|
338
|
+
};
|
|
339
|
+
oldSum = newSum;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return result;
|
|
343
|
+
}
|
|
344
|
+
static _getValue(series, when) {
|
|
345
|
+
switch (series.fn) {
|
|
346
|
+
case "Single": return this._getSingleValue(series, when);
|
|
347
|
+
case "DssVest": return this._getVestValue(series, when);
|
|
348
|
+
default: return 0;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
static _getSingleValue(series, when) {
|
|
352
|
+
return when >= series.start ? series.value : 0;
|
|
353
|
+
}
|
|
354
|
+
static _getVestValue(series, when) {
|
|
355
|
+
const now = when;
|
|
356
|
+
const start = series.start;
|
|
357
|
+
const end = series.end;
|
|
358
|
+
const cliff = series.params?.cliff ? DateTime.fromISO(series.params.cliff) : null;
|
|
359
|
+
if (now < start || cliff && now < cliff) return 0;
|
|
360
|
+
else if (end && now >= end) return series.value;
|
|
361
|
+
const a = Interval.fromDateTimes(start, now);
|
|
362
|
+
const b = Interval.fromDateTimes(start, end || now);
|
|
363
|
+
return a.length() / b.length() * series.value;
|
|
364
|
+
}
|
|
365
|
+
static _buildIndex(series, dimensions) {
|
|
366
|
+
const result = {};
|
|
367
|
+
const map = {};
|
|
368
|
+
const dimName = dimensions[0] || "";
|
|
369
|
+
for (const s of series) {
|
|
370
|
+
const dimValue = s.dimensions[dimName];
|
|
371
|
+
if (void 0 === map[dimValue]) map[dimValue] = [];
|
|
372
|
+
map[dimValue].push(s);
|
|
373
|
+
}
|
|
374
|
+
if (dimensions.length > 1) {
|
|
375
|
+
const newDimensions = dimensions.slice(1);
|
|
376
|
+
Object.keys(map).forEach((k) => {
|
|
377
|
+
result[k] = this._buildIndex(map[k], newDimensions);
|
|
378
|
+
});
|
|
379
|
+
} else Object.keys(map).forEach((k) => {
|
|
380
|
+
result[k] = this._buildMetricsIndex(map[k]);
|
|
381
|
+
});
|
|
382
|
+
return result;
|
|
383
|
+
}
|
|
384
|
+
static _buildMetricsIndex(series) {
|
|
385
|
+
const result = {};
|
|
386
|
+
const map = {};
|
|
387
|
+
for (const s of series) {
|
|
388
|
+
const metric = s.metric;
|
|
389
|
+
if (void 0 === map[metric]) map[metric] = [];
|
|
390
|
+
map[metric].push(s);
|
|
391
|
+
}
|
|
392
|
+
Object.keys(map).forEach((k) => result[k] = this._buildUnitIndex(map[k]));
|
|
393
|
+
return result;
|
|
394
|
+
}
|
|
395
|
+
static _buildUnitIndex(series) {
|
|
396
|
+
const result = {};
|
|
397
|
+
for (const s of series) {
|
|
398
|
+
const unit = s.unit || "__NULL__";
|
|
399
|
+
if (void 0 === result[unit]) result[unit] = [];
|
|
400
|
+
result[unit].push(s);
|
|
401
|
+
}
|
|
402
|
+
return result;
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
//#endregion
|
|
406
|
+
//#region src/AnalyticsQueryEngine.ts
|
|
407
|
+
var AnalyticsQueryEngine = class {
|
|
408
|
+
_profiler;
|
|
409
|
+
constructor(_analyticsStore, profiler) {
|
|
410
|
+
this._analyticsStore = _analyticsStore;
|
|
411
|
+
this._profiler = profiler ?? new PassthroughAnalyticsProfiler();
|
|
412
|
+
}
|
|
413
|
+
async executeCompound(query) {
|
|
414
|
+
let result;
|
|
415
|
+
const inputsQuery = {
|
|
416
|
+
start: query.start,
|
|
417
|
+
end: query.end,
|
|
418
|
+
granularity: query.granularity,
|
|
419
|
+
lod: query.lod,
|
|
420
|
+
select: query.select,
|
|
421
|
+
metrics: query.expression.inputs.metrics,
|
|
422
|
+
currency: query.expression.inputs.currency
|
|
423
|
+
};
|
|
424
|
+
const operandQuery = {
|
|
425
|
+
start: query.start,
|
|
426
|
+
end: query.end,
|
|
427
|
+
granularity: query.granularity,
|
|
428
|
+
lod: { priceData: 1 },
|
|
429
|
+
select: { priceData: [AnalyticsPath.fromString("atlas")] },
|
|
430
|
+
metrics: [query.expression.operand.metric],
|
|
431
|
+
currency: query.expression.operand.currency
|
|
432
|
+
};
|
|
433
|
+
const inputExecute = await this.execute(inputsQuery);
|
|
434
|
+
const operandExecute = await this.execute(operandQuery);
|
|
435
|
+
if ([CompoundOperator.VectorAdd, CompoundOperator.VectorSubtract].includes(query.expression.operator)) result = await this._profiler.record("ApplyVectorOperator", async () => await this._applyVectorOperator(inputExecute, operandExecute, query.expression.operator, query.expression.resultCurrency));
|
|
436
|
+
else result = await this._profiler.record("ApplyScalarOperator", async () => await this._applyScalarOperator(inputExecute, operandExecute, query.expression.operator, query.expression.operand.useSum, query.expression.resultCurrency));
|
|
437
|
+
return result;
|
|
438
|
+
}
|
|
439
|
+
async execute(query) {
|
|
440
|
+
const seriesResults = await this._executeSeriesQuery(query);
|
|
441
|
+
const normalizedSeriesResults = this._profiler.recordSync("ApplyLODs", () => this._applyLods(seriesResults, query.lod));
|
|
442
|
+
const dimensions = Object.keys(query.select);
|
|
443
|
+
return this._profiler.recordSync("Discretize", () => normalizedSeriesResults.length < 1 ? [] : AnalyticsDiscretizer.discretize(normalizedSeriesResults, dimensions, query.start, query.end, query.granularity));
|
|
444
|
+
}
|
|
445
|
+
async executeMultiCurrency(query, mcc) {
|
|
446
|
+
const baseQuery = {
|
|
447
|
+
...query,
|
|
448
|
+
currency: mcc.targetCurrency ?? query.currency
|
|
449
|
+
};
|
|
450
|
+
let result = await this.execute(baseQuery);
|
|
451
|
+
for (const conversion of mcc.conversions) {
|
|
452
|
+
const nextQuery = {
|
|
453
|
+
start: query.start,
|
|
454
|
+
end: query.end,
|
|
455
|
+
granularity: query.granularity,
|
|
456
|
+
lod: query.lod,
|
|
457
|
+
select: query.select,
|
|
458
|
+
expression: {
|
|
459
|
+
inputs: {
|
|
460
|
+
metrics: baseQuery.metrics,
|
|
461
|
+
currency: conversion.currency
|
|
462
|
+
},
|
|
463
|
+
operator: CompoundOperator.ScalarMultiply,
|
|
464
|
+
operand: {
|
|
465
|
+
metric: conversion.metric,
|
|
466
|
+
currency: mcc.targetCurrency,
|
|
467
|
+
useSum: true
|
|
468
|
+
},
|
|
469
|
+
resultCurrency: mcc.targetCurrency
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
const executedCompound = await this.executeCompound(nextQuery);
|
|
473
|
+
result = await this._applyVectorOperator(result, executedCompound, CompoundOperator.VectorAdd, mcc.targetCurrency);
|
|
474
|
+
}
|
|
475
|
+
return result;
|
|
476
|
+
}
|
|
477
|
+
async _applyVectorOperator(inputsA, inputsB, operator, resultCurrency) {
|
|
478
|
+
if ([CompoundOperator.ScalarMultiply, CompoundOperator.ScalarDivide].includes(operator)) throw new Error("Invalid operator for vector operation");
|
|
479
|
+
return inputsB;
|
|
480
|
+
}
|
|
481
|
+
async _applyScalarOperator(inputs, operand, operator, useOperandSum, resultCurrency) {
|
|
482
|
+
if ([CompoundOperator.VectorAdd, CompoundOperator.VectorSubtract].includes(operator)) throw new Error("Invalid operator for scalar operation");
|
|
483
|
+
const result = [];
|
|
484
|
+
const operandMap = {};
|
|
485
|
+
const key = useOperandSum ? "sum" : "value";
|
|
486
|
+
for (const operandPeriod of operand) if (operandPeriod.rows.length > 0) operandMap[operandPeriod.period] = operandPeriod.rows[0][key];
|
|
487
|
+
for (const inputPeriod of inputs) {
|
|
488
|
+
const outputPeriod = {
|
|
489
|
+
period: inputPeriod.period,
|
|
490
|
+
start: inputPeriod.start,
|
|
491
|
+
end: inputPeriod.end,
|
|
492
|
+
rows: inputPeriod.rows.map((row) => {
|
|
493
|
+
return {
|
|
494
|
+
dimensions: row.dimensions,
|
|
495
|
+
metric: row.metric,
|
|
496
|
+
unit: resultCurrency ? resultCurrency.toString() : row.unit,
|
|
497
|
+
value: this._calculateOutputValue(row.value, operator, operandMap[inputPeriod.period]),
|
|
498
|
+
sum: -1
|
|
499
|
+
};
|
|
500
|
+
})
|
|
501
|
+
};
|
|
502
|
+
result.push(outputPeriod);
|
|
503
|
+
}
|
|
504
|
+
return result;
|
|
505
|
+
}
|
|
506
|
+
_calculateOutputValue(input, operator, operand) {
|
|
507
|
+
switch (operator) {
|
|
508
|
+
case CompoundOperator.VectorAdd: return input + operand;
|
|
509
|
+
case CompoundOperator.VectorSubtract: return input - operand;
|
|
510
|
+
case CompoundOperator.ScalarMultiply: return input * operand;
|
|
511
|
+
case CompoundOperator.ScalarDivide: return input / operand;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
async _executeSeriesQuery(query) {
|
|
515
|
+
const seriesQuery = {
|
|
516
|
+
start: query.start,
|
|
517
|
+
end: query.end,
|
|
518
|
+
select: query.select,
|
|
519
|
+
metrics: query.metrics,
|
|
520
|
+
currency: query.currency
|
|
521
|
+
};
|
|
522
|
+
return await this._analyticsStore.getMatchingSeries(seriesQuery);
|
|
523
|
+
}
|
|
524
|
+
_applyLods(series, lods) {
|
|
525
|
+
return series.map((result) => ({
|
|
526
|
+
...result,
|
|
527
|
+
dimensions: this._applyDimensionsLods(result.dimensions, lods)
|
|
528
|
+
}));
|
|
529
|
+
}
|
|
530
|
+
_applyDimensionsLods(dimensionMap, lods) {
|
|
531
|
+
const result = {};
|
|
532
|
+
for (const [dimension, lod] of Object.entries(lods)) if (lod !== null && dimensionMap[dimension]) {
|
|
533
|
+
result[dimension] = dimensionMap[dimension]["path"].applyLod(lod).toString();
|
|
534
|
+
result["icon"] = dimensionMap[dimension]["icon"].toString();
|
|
535
|
+
result["label"] = dimensionMap[dimension]["label"].toString();
|
|
536
|
+
result["description"] = dimensionMap[dimension]["description"].toString();
|
|
537
|
+
}
|
|
538
|
+
return result;
|
|
539
|
+
}
|
|
540
|
+
async getDimensions() {
|
|
541
|
+
return await this._analyticsStore.getDimensions();
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
//#endregion
|
|
545
|
+
//#region src/AnalyticsSubscriptionManager.ts
|
|
546
|
+
var NotificationError = class extends Error {
|
|
547
|
+
innerErrors;
|
|
548
|
+
constructor(errors) {
|
|
549
|
+
super(errors.map((e) => e.message).join("\n"));
|
|
550
|
+
this.name = "NotificationError";
|
|
551
|
+
this.innerErrors = errors;
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
/**
|
|
555
|
+
* Manages subscriptions for analytics paths.
|
|
556
|
+
*/
|
|
557
|
+
var AnalyticsSubscriptionManager = class {
|
|
558
|
+
_subscriptions = /* @__PURE__ */ new Map();
|
|
559
|
+
/**
|
|
560
|
+
* Subscribe to updates for an analytics path. A subscribed function will be
|
|
561
|
+
* called for:
|
|
562
|
+
*
|
|
563
|
+
* - exact path matches
|
|
564
|
+
* - matching child paths (i.e. an update to /a/b/c will trigger a callback
|
|
565
|
+
* for /a/b/c, /a/b, and /a)
|
|
566
|
+
* - wildcard matches
|
|
567
|
+
*
|
|
568
|
+
* @param path The analytics path to subscribe to.
|
|
569
|
+
* @param callback Function to be called when the path is updated.
|
|
570
|
+
*
|
|
571
|
+
* @returns A function that, when called, unsubscribes from the updates.
|
|
572
|
+
*/
|
|
573
|
+
subscribeToPath(path, callback) {
|
|
574
|
+
const pathString = this.normalizePath(path.toString("/"));
|
|
575
|
+
if (!this._subscriptions.has(pathString)) this._subscriptions.set(pathString, /* @__PURE__ */ new Set());
|
|
576
|
+
this._subscriptions.get(pathString).add(callback);
|
|
577
|
+
return () => {
|
|
578
|
+
const callbacks = this._subscriptions.get(pathString);
|
|
579
|
+
if (callbacks) {
|
|
580
|
+
callbacks.delete(callback);
|
|
581
|
+
if (callbacks.size === 0) this._subscriptions.delete(pathString);
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Notifies subscribers about updates to paths.
|
|
587
|
+
*
|
|
588
|
+
* @param paths The paths that were updated.
|
|
589
|
+
*/
|
|
590
|
+
notifySubscribers(paths) {
|
|
591
|
+
if (paths.length === 0 || this._subscriptions.size === 0) return;
|
|
592
|
+
const errors = [];
|
|
593
|
+
for (const path of paths) {
|
|
594
|
+
const pathString = this.normalizePath(path.toString("/"));
|
|
595
|
+
const pathPrefixes = this.getPathPrefixes(pathString);
|
|
596
|
+
const matchingSubscriptions = [];
|
|
597
|
+
pathPrefixes.filter((prefix) => this._subscriptions.has(prefix)).forEach((prefix) => {
|
|
598
|
+
matchingSubscriptions.push({
|
|
599
|
+
prefix,
|
|
600
|
+
callbacks: this._subscriptions.get(prefix)
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
for (const [subscriptionPath, callbacks] of this._subscriptions.entries()) {
|
|
604
|
+
if (pathPrefixes.includes(subscriptionPath)) continue;
|
|
605
|
+
if (this.pathMatchesWildcardPattern(pathString, subscriptionPath)) matchingSubscriptions.push({
|
|
606
|
+
prefix: subscriptionPath,
|
|
607
|
+
callbacks
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
if (matchingSubscriptions.length === 0) continue;
|
|
611
|
+
for (const { callbacks } of matchingSubscriptions) for (const callback of callbacks) try {
|
|
612
|
+
callback(path);
|
|
613
|
+
} catch (e) {
|
|
614
|
+
errors.push(e);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
if (errors.length > 0) throw new NotificationError(errors);
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Normalizes a path string to ensure consistent comparison.
|
|
621
|
+
*/
|
|
622
|
+
normalizePath(path) {
|
|
623
|
+
return "/" + path.split("/").filter((p) => p.length > 0).join("/");
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Checks if a path matches a subscription pattern that may contain wildcards.
|
|
627
|
+
*/
|
|
628
|
+
pathMatchesWildcardPattern(updatePath, subscriptionPath) {
|
|
629
|
+
if (subscriptionPath.includes("*")) {
|
|
630
|
+
const updateSegments = updatePath.split("/").filter((s) => s.length > 0);
|
|
631
|
+
const subscriptionSegments = subscriptionPath.split("/").filter((s) => s.length > 0);
|
|
632
|
+
if (subscriptionSegments.length > updateSegments.length) return false;
|
|
633
|
+
for (let i = 0; i < subscriptionSegments.length; i++) {
|
|
634
|
+
const subSegment = subscriptionSegments[i];
|
|
635
|
+
const updateSegment = updateSegments[i];
|
|
636
|
+
if (subSegment === "*" || subSegment.startsWith("*:")) continue;
|
|
637
|
+
if (subSegment !== updateSegment) return false;
|
|
638
|
+
}
|
|
639
|
+
return true;
|
|
640
|
+
}
|
|
641
|
+
return updatePath === subscriptionPath || updatePath.startsWith(subscriptionPath + "/");
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Gets all path prefixes for a given path.
|
|
645
|
+
*
|
|
646
|
+
* For example, for '/a/b/c' it returns ['/a', '/a/b', '/a/b/c'].
|
|
647
|
+
*/
|
|
648
|
+
getPathPrefixes(path) {
|
|
649
|
+
const segments = path.split("/").filter((s) => s.length > 0);
|
|
650
|
+
const prefixes = [];
|
|
651
|
+
let currentPath = "";
|
|
652
|
+
for (const segment of segments) {
|
|
653
|
+
currentPath = currentPath ? `${currentPath}/${segment}` : `/${segment}`;
|
|
654
|
+
prefixes.push(currentPath);
|
|
655
|
+
}
|
|
656
|
+
if (prefixes.length === 0 && path === "/") prefixes.push("/");
|
|
657
|
+
return prefixes;
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
//#endregion
|
|
661
|
+
//#region src/util.ts
|
|
662
|
+
const defaultQueryLogger = (tag) => (index, query) => {
|
|
663
|
+
console.log(`[${tag}][Q:${index}]: ${query}\n`);
|
|
664
|
+
};
|
|
665
|
+
const defaultResultsLogger = (tag) => (index, results) => {
|
|
666
|
+
if (Array.isArray(results)) console.log(`[${tag}][R:${index}]: ${results.length} results\n`);
|
|
667
|
+
else console.log(`[${tag}][R:${index}]: Received ${typeof results}.\n`);
|
|
668
|
+
};
|
|
669
|
+
//#endregion
|
|
670
|
+
export { AnalyticsGranularity, AnalyticsPath, AnalyticsPathSegment, AnalyticsPeriod, AnalyticsProfiler, AnalyticsQueryEngine, AnalyticsSerializerTypes, AnalyticsSubscriptionManager, NotificationError, PassthroughAnalyticsProfiler, defaultQueryLogger, defaultResultsLogger };
|
|
671
|
+
|
|
672
|
+
//# sourceMappingURL=index.js.map
|