@powerhousedao/analytics-engine-core 6.0.0-dev.104 → 6.0.0-dev.106

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.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