@contractspec/lib.metering 0.0.0-canary-20260113162409

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Chaman Ventures, SASU
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,198 @@
1
+ # @contractspec/lib.metering
2
+
3
+ Website: https://contractspec.io/
4
+
5
+
6
+ Usage metering and billing core module for ContractSpec applications.
7
+
8
+ ## Overview
9
+
10
+ This module provides a reusable metering system that can be used to track usage-based metrics across all ContractSpec applications. It supports:
11
+
12
+ - **Metric Definitions**: Define what metrics to track
13
+ - **Usage Recording**: Record usage events
14
+ - **Aggregation**: Roll up usage into summaries
15
+ - **Thresholds & Alerts**: Monitor usage against limits
16
+ - **Billing Integration**: Connect usage to billing/plans
17
+
18
+ ## Entities
19
+
20
+ ### MetricDefinition
21
+
22
+ Defines a trackable metric.
23
+
24
+ | Field | Type | Description |
25
+ |-------|------|-------------|
26
+ | id | string | Unique identifier |
27
+ | key | string | Metric key (e.g., `api_calls`, `storage_gb`) |
28
+ | name | string | Human-readable name |
29
+ | description | string | Metric description |
30
+ | unit | string | Unit of measurement |
31
+ | aggregationType | enum | How to aggregate (count, sum, avg, max, min) |
32
+ | resetPeriod | enum | When to reset (never, hourly, daily, monthly) |
33
+ | orgId | string | Organization scope (null = global) |
34
+
35
+ ### UsageRecord
36
+
37
+ Individual usage event.
38
+
39
+ | Field | Type | Description |
40
+ |-------|------|-------------|
41
+ | id | string | Unique identifier |
42
+ | metricKey | string | Metric being recorded |
43
+ | subjectType | string | Subject type (org, user, project) |
44
+ | subjectId | string | Subject identifier |
45
+ | quantity | decimal | Usage quantity |
46
+ | timestamp | datetime | When usage occurred |
47
+ | metadata | json | Additional context |
48
+
49
+ ### UsageSummary
50
+
51
+ Pre-aggregated usage summary.
52
+
53
+ | Field | Type | Description |
54
+ |-------|------|-------------|
55
+ | id | string | Unique identifier |
56
+ | metricKey | string | Metric key |
57
+ | subjectType | string | Subject type |
58
+ | subjectId | string | Subject identifier |
59
+ | periodType | enum | Period type (hourly, daily, monthly) |
60
+ | periodStart | datetime | Period start time |
61
+ | periodEnd | datetime | Period end time |
62
+ | totalQuantity | decimal | Aggregated quantity |
63
+ | recordCount | int | Number of records aggregated |
64
+
65
+ ### UsageThreshold
66
+
67
+ Threshold configuration for alerts.
68
+
69
+ | Field | Type | Description |
70
+ |-------|------|-------------|
71
+ | id | string | Unique identifier |
72
+ | metricKey | string | Metric to monitor |
73
+ | subjectType | string | Subject type |
74
+ | subjectId | string | Subject identifier |
75
+ | threshold | decimal | Threshold value |
76
+ | action | enum | Action when exceeded (alert, block, none) |
77
+ | notifyEmails | json | Email addresses to notify |
78
+
79
+ ## Contracts
80
+
81
+ ### Commands
82
+
83
+ - `metric.define` - Define a new metric
84
+ - `metric.update` - Update metric definition
85
+ - `metric.delete` - Delete a metric
86
+ - `usage.record` - Record a usage event
87
+ - `usage.recordBatch` - Record multiple usage events
88
+ - `threshold.create` - Create a usage threshold
89
+ - `threshold.update` - Update a threshold
90
+ - `threshold.delete` - Delete a threshold
91
+
92
+ ### Queries
93
+
94
+ - `metric.get` - Get metric definition
95
+ - `metric.list` - List all metrics
96
+ - `usage.get` - Get usage for a subject
97
+ - `usage.getSummary` - Get aggregated usage summary
98
+ - `usage.export` - Export usage data
99
+ - `threshold.list` - List thresholds
100
+
101
+ ## Events
102
+
103
+ - `metric.defined` - Metric was defined
104
+ - `usage.recorded` - Usage was recorded
105
+ - `usage.aggregated` - Usage was aggregated into summary
106
+ - `threshold.exceeded` - Usage exceeded a threshold
107
+ - `threshold.approaching` - Usage approaching threshold (80%)
108
+
109
+ ## Usage
110
+
111
+ ```typescript
112
+ import {
113
+ MetricDefinitionEntity,
114
+ RecordUsageContract,
115
+ UsageAggregator
116
+ } from '@contractspec/lib.metering';
117
+
118
+ // Define a metric
119
+ await meteringService.defineMetric({
120
+ key: 'api_calls',
121
+ name: 'API Calls',
122
+ unit: 'calls',
123
+ aggregationType: 'COUNT',
124
+ resetPeriod: 'MONTHLY',
125
+ });
126
+
127
+ // Record usage
128
+ await meteringService.recordUsage({
129
+ metricKey: 'api_calls',
130
+ subjectType: 'org',
131
+ subjectId: 'org-123',
132
+ quantity: 1,
133
+ });
134
+
135
+ // Get usage summary
136
+ const summary = await meteringService.getUsageSummary({
137
+ metricKey: 'api_calls',
138
+ subjectType: 'org',
139
+ subjectId: 'org-123',
140
+ periodType: 'MONTHLY',
141
+ periodStart: new Date('2024-01-01'),
142
+ });
143
+ ```
144
+
145
+ ## Aggregation
146
+
147
+ The module includes an aggregation engine that periodically rolls up usage records:
148
+
149
+ ```typescript
150
+ import { UsageAggregator } from '@contractspec/lib.metering/aggregation';
151
+
152
+ const aggregator = new UsageAggregator({
153
+ storage: usageStorage,
154
+ });
155
+
156
+ // Run hourly aggregation
157
+ await aggregator.aggregate({
158
+ periodType: 'HOURLY',
159
+ periodStart: new Date(),
160
+ });
161
+ ```
162
+
163
+ ## Integration
164
+
165
+ This module integrates with:
166
+
167
+ - `@contractspec/lib.jobs` - Scheduled aggregation jobs
168
+ - `@contractspec/module.notifications` - Threshold alerts
169
+ - `@contractspec/lib.identity-rbac` - Subject resolution
170
+
171
+ ## Schema Contribution
172
+
173
+ ```typescript
174
+ import { meteringSchemaContribution } from '@contractspec/lib.metering';
175
+
176
+ export const schemaComposition = {
177
+ modules: [
178
+ meteringSchemaContribution,
179
+ // ... other modules
180
+ ],
181
+ };
182
+ ```
183
+
184
+
185
+
186
+
187
+
188
+
189
+
190
+
191
+
192
+
193
+
194
+
195
+
196
+
197
+
198
+
@@ -0,0 +1,156 @@
1
+ //#region src/aggregation/index.d.ts
2
+ /**
3
+ * Usage aggregation engine.
4
+ *
5
+ * Provides periodic aggregation of usage records into summaries
6
+ * for efficient billing and reporting queries.
7
+ */
8
+ type PeriodType = 'HOURLY' | 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'YEARLY';
9
+ type AggregationType = 'COUNT' | 'SUM' | 'AVG' | 'MAX' | 'MIN' | 'LAST';
10
+ interface UsageRecord {
11
+ id: string;
12
+ metricKey: string;
13
+ subjectType: string;
14
+ subjectId: string;
15
+ quantity: number;
16
+ timestamp: Date;
17
+ }
18
+ interface UsageSummary {
19
+ id: string;
20
+ metricKey: string;
21
+ subjectType: string;
22
+ subjectId: string;
23
+ periodType: PeriodType;
24
+ periodStart: Date;
25
+ periodEnd: Date;
26
+ totalQuantity: number;
27
+ recordCount: number;
28
+ minQuantity?: number;
29
+ maxQuantity?: number;
30
+ avgQuantity?: number;
31
+ }
32
+ interface MetricDefinition {
33
+ key: string;
34
+ aggregationType: AggregationType;
35
+ }
36
+ interface UsageStorage {
37
+ /**
38
+ * Get unaggregated records for a period.
39
+ */
40
+ getUnaggregatedRecords(options: {
41
+ metricKey?: string;
42
+ periodStart: Date;
43
+ periodEnd: Date;
44
+ limit?: number;
45
+ }): Promise<UsageRecord[]>;
46
+ /**
47
+ * Mark records as aggregated.
48
+ */
49
+ markRecordsAggregated(recordIds: string[], aggregatedAt: Date): Promise<void>;
50
+ /**
51
+ * Get or create a summary record.
52
+ */
53
+ upsertSummary(summary: Omit<UsageSummary, 'id'>): Promise<UsageSummary>;
54
+ /**
55
+ * Get metric definition.
56
+ */
57
+ getMetric(key: string): Promise<MetricDefinition | null>;
58
+ /**
59
+ * List all active metrics.
60
+ */
61
+ listMetrics(): Promise<MetricDefinition[]>;
62
+ }
63
+ interface AggregationOptions {
64
+ /** Storage implementation */
65
+ storage: UsageStorage;
66
+ /** Batch size for processing records */
67
+ batchSize?: number;
68
+ }
69
+ interface AggregateParams {
70
+ /** Period type to aggregate */
71
+ periodType: PeriodType;
72
+ /** Period start time */
73
+ periodStart: Date;
74
+ /** Period end time (optional, defaults to period boundary) */
75
+ periodEnd?: Date;
76
+ /** Specific metric to aggregate (optional, aggregates all if not specified) */
77
+ metricKey?: string;
78
+ }
79
+ interface AggregationResult {
80
+ periodType: PeriodType;
81
+ periodStart: Date;
82
+ periodEnd: Date;
83
+ recordsProcessed: number;
84
+ summariesCreated: number;
85
+ summariesUpdated: number;
86
+ errors: AggregationError[];
87
+ }
88
+ interface AggregationError {
89
+ metricKey: string;
90
+ subjectType: string;
91
+ subjectId: string;
92
+ error: string;
93
+ }
94
+ /**
95
+ * Get the start of a period for a given date.
96
+ */
97
+ declare function getPeriodStart(date: Date, periodType: PeriodType): Date;
98
+ /**
99
+ * Get the end of a period for a given date.
100
+ */
101
+ declare function getPeriodEnd(date: Date, periodType: PeriodType): Date;
102
+ /**
103
+ * Format a period key for grouping.
104
+ */
105
+ declare function formatPeriodKey(date: Date, periodType: PeriodType): string;
106
+ /**
107
+ * Usage aggregator.
108
+ *
109
+ * Aggregates usage records into summaries based on period type.
110
+ */
111
+ declare class UsageAggregator {
112
+ private storage;
113
+ private batchSize;
114
+ constructor(options: AggregationOptions);
115
+ /**
116
+ * Aggregate usage records for a period.
117
+ */
118
+ aggregate(params: AggregateParams): Promise<AggregationResult>;
119
+ /**
120
+ * Group records by metric, subject, and period.
121
+ */
122
+ private groupRecords;
123
+ /**
124
+ * Aggregate a group of records into a summary.
125
+ */
126
+ private aggregateGroup;
127
+ /**
128
+ * Calculate aggregation values.
129
+ */
130
+ private calculateAggregation;
131
+ }
132
+ /**
133
+ * In-memory usage storage for testing.
134
+ */
135
+ declare class InMemoryUsageStorage implements UsageStorage {
136
+ private records;
137
+ private summaries;
138
+ private metrics;
139
+ addRecord(record: UsageRecord): void;
140
+ addMetric(metric: MetricDefinition): void;
141
+ getUnaggregatedRecords(options: {
142
+ metricKey?: string;
143
+ periodStart: Date;
144
+ periodEnd: Date;
145
+ limit?: number;
146
+ }): Promise<UsageRecord[]>;
147
+ markRecordsAggregated(recordIds: string[]): Promise<void>;
148
+ upsertSummary(summary: Omit<UsageSummary, 'id'>): Promise<UsageSummary>;
149
+ getMetric(key: string): Promise<MetricDefinition | null>;
150
+ listMetrics(): Promise<MetricDefinition[]>;
151
+ getSummaries(): UsageSummary[];
152
+ clear(): void;
153
+ }
154
+ //#endregion
155
+ export { AggregateParams, AggregationError, AggregationOptions, AggregationResult, AggregationType, InMemoryUsageStorage, MetricDefinition, PeriodType, UsageAggregator, UsageRecord, UsageStorage, UsageSummary, formatPeriodKey, getPeriodEnd, getPeriodStart };
156
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/aggregation/index.ts"],"sourcesContent":[],"mappings":";;AASA;AACA;AAEA;AASA;;AAMe,KAlBH,UAAA,GAkBG,QAAA,GAAA,OAAA,GAAA,QAAA,GAAA,SAAA,GAAA,QAAA;AACF,KAlBD,eAAA,GAkBC,OAAA,GAAA,KAAA,GAAA,KAAA,GAAA,KAAA,GAAA,KAAA,GAAA,MAAA;AAAI,UAhBA,WAAA,CAgBA;EAQA,EAAA,EAAA,MAAA;EAKA,SAAA,EAAA,MAAY;EAMZ,WAAA,EAAA,MAAA;EACF,SAAA,EAAA,MAAA;EAED,QAAA,EAAA,MAAA;EAAR,SAAA,EAhCO,IAgCP;;AAK4D,UAlCjD,YAAA,CAkCiD;EAKpC,EAAA,EAAA,MAAA;EAAL,SAAA,EAAA,MAAA;EAAmC,WAAA,EAAA,MAAA;EAAR,SAAA,EAAA,MAAA;EAKlB,UAAA,EAvCpB,UAuCoB;EAAR,WAAA,EAtCX,IAsCW;EAKD,SAAA,EA1CZ,IA0CY;EAAR,aAAA,EAAA,MAAA;EAAO,WAAA,EAAA,MAAA;EAGP,WAAA,CAAA,EAAA,MAAA;EAOA,WAAA,CAAA,EAAA,MAAe;EAElB,WAAA,CAAA,EAAA,MAAA;;AAIA,UAlDG,gBAAA,CAkDH;EAAI,GAAA,EAAA,MAAA;EAKD,eAAA,EArDE,eAqDe;;AAEnB,UApDE,YAAA,CAoDF;EACF;;;EAOI,sBAAgB,CAAA,OAAA,EAAA;IAYjB,SAAA,CAAA,EAAA,MAAc;IAAO,WAAA,EAlEpB,IAkEoB;IAAkB,SAAA,EAjExC,IAiEwC;IAAa,KAAA,CAAA,EAAA,MAAA;EAAI,CAAA,CAAA,EA/DlE,OA+DkE,CA/D1D,WA+D0D,EAAA,CAAA;EAkCxD;;;EAAkD,qBAAA,CAAA,SAAA,EAAA,MAAA,EAAA,EAAA,YAAA,EA5FP,IA4FO,CAAA,EA5FA,OA4FA,CAAA,IAAA,CAAA;EAAI;AA8BtE;AAyCA;EAIuB,aAAA,CAAA,OAAA,EAlKE,IAkKF,CAlKO,YAkKP,EAAA,IAAA,CAAA,CAAA,EAlK6B,OAkK7B,CAlKqC,YAkKrC,CAAA;EAQG;;;EAAyB,SAAA,CAAA,GAAA,EAAA,MAAA,CAAA,EArKzB,OAqKyB,CArKjB,gBAqKiB,GAAA,IAAA,CAAA;EAyKtC;;;EAeI,WAAA,EAAA,EAxVA,OAwVA,CAxVQ,gBAwVR,EAAA,CAAA;;AAGH,UAxVG,kBAAA,CAwVH;EAAR;EAgB8C,OAAA,EAtWzC,YAsWyC;EAKlC;EAAL,SAAA,CAAA,EAAA,MAAA;;AACR,UAvWY,eAAA,CAuWZ;EAgCmC;EAAR,UAAA,EArYlB,UAqYkB;EAID;EAAR,WAAA,EAvYR,IAuYQ;EAIL;EAhF2B,SAAA,CAAA,EAzT/B,IAyT+B;EAAY;;;UApTxC,iBAAA;cACH;eACC;aACF;;;;UAIH;;UAGO,gBAAA;;;;;;;;;iBAYD,cAAA,OAAqB,kBAAkB,aAAa;;;;iBAkCpD,YAAA,OAAmB,kBAAkB,aAAa;;;;iBA8BlD,eAAA,OAAsB,kBAAkB;;;;;;cAyC3C,eAAA;;;uBAIU;;;;oBAQG,kBAAkB,QAAQ;;;;;;;;;;;;;;;;;cAyKvC,oBAAA,YAAgC;;;;oBAKzB;oBAIA;;;iBAMH;eACF;;MAET,QAAQ;8CAgBsC;yBAKvC,KAAK,sBACb,QAAQ;0BAgCmB,QAAQ;iBAIjB,QAAQ;kBAIb"}
@@ -0,0 +1,274 @@
1
+ //#region src/aggregation/index.ts
2
+ /**
3
+ * Get the start of a period for a given date.
4
+ */
5
+ function getPeriodStart(date, periodType) {
6
+ const d = new Date(date);
7
+ switch (periodType) {
8
+ case "HOURLY":
9
+ d.setMinutes(0, 0, 0);
10
+ return d;
11
+ case "DAILY":
12
+ d.setHours(0, 0, 0, 0);
13
+ return d;
14
+ case "WEEKLY": {
15
+ d.setHours(0, 0, 0, 0);
16
+ const day = d.getDay();
17
+ d.setDate(d.getDate() - day);
18
+ return d;
19
+ }
20
+ case "MONTHLY":
21
+ d.setHours(0, 0, 0, 0);
22
+ d.setDate(1);
23
+ return d;
24
+ case "YEARLY":
25
+ d.setHours(0, 0, 0, 0);
26
+ d.setMonth(0, 1);
27
+ return d;
28
+ }
29
+ }
30
+ /**
31
+ * Get the end of a period for a given date.
32
+ */
33
+ function getPeriodEnd(date, periodType) {
34
+ const start = getPeriodStart(date, periodType);
35
+ switch (periodType) {
36
+ case "HOURLY": return new Date(start.getTime() + 3600 * 1e3);
37
+ case "DAILY": return new Date(start.getTime() + 1440 * 60 * 1e3);
38
+ case "WEEKLY": return new Date(start.getTime() + 10080 * 60 * 1e3);
39
+ case "MONTHLY": {
40
+ const end = new Date(start);
41
+ end.setMonth(end.getMonth() + 1);
42
+ return end;
43
+ }
44
+ case "YEARLY": {
45
+ const end = new Date(start);
46
+ end.setFullYear(end.getFullYear() + 1);
47
+ return end;
48
+ }
49
+ }
50
+ }
51
+ /**
52
+ * Format a period key for grouping.
53
+ */
54
+ function formatPeriodKey(date, periodType) {
55
+ const start = getPeriodStart(date, periodType);
56
+ const year = start.getFullYear();
57
+ const month = String(start.getMonth() + 1).padStart(2, "0");
58
+ const day = String(start.getDate()).padStart(2, "0");
59
+ const hour = String(start.getHours()).padStart(2, "0");
60
+ switch (periodType) {
61
+ case "HOURLY": return `${year}-${month}-${day}T${hour}`;
62
+ case "DAILY": return `${year}-${month}-${day}`;
63
+ case "WEEKLY": return `${year}-W${getWeekNumber(start)}`;
64
+ case "MONTHLY": return `${year}-${month}`;
65
+ case "YEARLY": return `${year}`;
66
+ }
67
+ }
68
+ function getWeekNumber(date) {
69
+ const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
70
+ const dayNum = d.getUTCDay() || 7;
71
+ d.setUTCDate(d.getUTCDate() + 4 - dayNum);
72
+ const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
73
+ const weekNum = Math.ceil(((d.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
74
+ return String(weekNum).padStart(2, "0");
75
+ }
76
+ /**
77
+ * Usage aggregator.
78
+ *
79
+ * Aggregates usage records into summaries based on period type.
80
+ */
81
+ var UsageAggregator = class {
82
+ storage;
83
+ batchSize;
84
+ constructor(options) {
85
+ this.storage = options.storage;
86
+ this.batchSize = options.batchSize || 1e3;
87
+ }
88
+ /**
89
+ * Aggregate usage records for a period.
90
+ */
91
+ async aggregate(params) {
92
+ const { periodType, periodStart, metricKey } = params;
93
+ const periodEnd = params.periodEnd || getPeriodEnd(periodStart, periodType);
94
+ const result = {
95
+ periodType,
96
+ periodStart,
97
+ periodEnd,
98
+ recordsProcessed: 0,
99
+ summariesCreated: 0,
100
+ summariesUpdated: 0,
101
+ errors: []
102
+ };
103
+ const records = await this.storage.getUnaggregatedRecords({
104
+ metricKey,
105
+ periodStart,
106
+ periodEnd,
107
+ limit: this.batchSize
108
+ });
109
+ if (records.length === 0) return result;
110
+ const groups = this.groupRecords(records, periodType);
111
+ for (const [groupKey, groupRecords] of groups.entries()) try {
112
+ await this.aggregateGroup(groupKey, groupRecords, periodType, result);
113
+ } catch (error) {
114
+ const [metricKey$1, subjectType, subjectId] = groupKey.split("::");
115
+ result.errors.push({
116
+ metricKey: metricKey$1 ?? "unknown",
117
+ subjectType: subjectType ?? "unknown",
118
+ subjectId: subjectId ?? "unknown",
119
+ error: error instanceof Error ? error.message : String(error)
120
+ });
121
+ }
122
+ const recordIds = records.map((r) => r.id);
123
+ await this.storage.markRecordsAggregated(recordIds, /* @__PURE__ */ new Date());
124
+ result.recordsProcessed = records.length;
125
+ return result;
126
+ }
127
+ /**
128
+ * Group records by metric, subject, and period.
129
+ */
130
+ groupRecords(records, periodType) {
131
+ const groups = /* @__PURE__ */ new Map();
132
+ for (const record of records) {
133
+ const periodKey = formatPeriodKey(record.timestamp, periodType);
134
+ const groupKey = `${record.metricKey}::${record.subjectType}::${record.subjectId}::${periodKey}`;
135
+ const existing = groups.get(groupKey) || [];
136
+ existing.push(record);
137
+ groups.set(groupKey, existing);
138
+ }
139
+ return groups;
140
+ }
141
+ /**
142
+ * Aggregate a group of records into a summary.
143
+ */
144
+ async aggregateGroup(groupKey, records, periodType, result) {
145
+ const [metricKey, subjectType, subjectId] = groupKey.split("::");
146
+ if (!metricKey || !subjectType || !subjectId || records.length === 0) return;
147
+ const firstRecord = records[0];
148
+ if (!firstRecord) return;
149
+ const periodStart = getPeriodStart(firstRecord.timestamp, periodType);
150
+ const periodEnd = getPeriodEnd(firstRecord.timestamp, periodType);
151
+ const aggregationType = (await this.storage.getMetric(metricKey))?.aggregationType || "SUM";
152
+ const quantities = records.map((r) => r.quantity);
153
+ const aggregated = this.calculateAggregation(quantities, aggregationType);
154
+ await this.storage.upsertSummary({
155
+ metricKey,
156
+ subjectType,
157
+ subjectId,
158
+ periodType,
159
+ periodStart,
160
+ periodEnd,
161
+ totalQuantity: aggregated.total,
162
+ recordCount: records.length,
163
+ minQuantity: aggregated.min,
164
+ maxQuantity: aggregated.max,
165
+ avgQuantity: aggregated.avg
166
+ });
167
+ result.summariesCreated++;
168
+ }
169
+ /**
170
+ * Calculate aggregation values.
171
+ */
172
+ calculateAggregation(quantities, aggregationType) {
173
+ if (quantities.length === 0) return {
174
+ total: 0,
175
+ min: 0,
176
+ max: 0,
177
+ avg: 0
178
+ };
179
+ const min = Math.min(...quantities);
180
+ const max = Math.max(...quantities);
181
+ const sum = quantities.reduce((a, b) => a + b, 0);
182
+ const avg = sum / quantities.length;
183
+ const count = quantities.length;
184
+ let total;
185
+ switch (aggregationType) {
186
+ case "COUNT":
187
+ total = count;
188
+ break;
189
+ case "SUM":
190
+ total = sum;
191
+ break;
192
+ case "AVG":
193
+ total = avg;
194
+ break;
195
+ case "MAX":
196
+ total = max;
197
+ break;
198
+ case "MIN":
199
+ total = min;
200
+ break;
201
+ case "LAST":
202
+ total = quantities[quantities.length - 1] ?? 0;
203
+ break;
204
+ default: total = sum;
205
+ }
206
+ return {
207
+ total,
208
+ min,
209
+ max,
210
+ avg
211
+ };
212
+ }
213
+ };
214
+ /**
215
+ * In-memory usage storage for testing.
216
+ */
217
+ var InMemoryUsageStorage = class {
218
+ records = [];
219
+ summaries = /* @__PURE__ */ new Map();
220
+ metrics = /* @__PURE__ */ new Map();
221
+ addRecord(record) {
222
+ this.records.push(record);
223
+ }
224
+ addMetric(metric) {
225
+ this.metrics.set(metric.key, metric);
226
+ }
227
+ async getUnaggregatedRecords(options) {
228
+ let records = this.records.filter((r) => {
229
+ const inPeriod = r.timestamp >= options.periodStart && r.timestamp < options.periodEnd;
230
+ const matchesMetric = !options.metricKey || r.metricKey === options.metricKey;
231
+ return inPeriod && matchesMetric;
232
+ });
233
+ if (options.limit) records = records.slice(0, options.limit);
234
+ return records;
235
+ }
236
+ async markRecordsAggregated(recordIds) {
237
+ this.records = this.records.filter((r) => !recordIds.includes(r.id));
238
+ }
239
+ async upsertSummary(summary) {
240
+ const key = `${summary.metricKey}::${summary.subjectType}::${summary.subjectId}::${summary.periodType}::${summary.periodStart.toISOString()}`;
241
+ const existing = this.summaries.get(key);
242
+ if (existing) {
243
+ existing.totalQuantity += summary.totalQuantity;
244
+ existing.recordCount += summary.recordCount;
245
+ if (summary.minQuantity !== void 0) existing.minQuantity = Math.min(existing.minQuantity ?? Infinity, summary.minQuantity);
246
+ if (summary.maxQuantity !== void 0) existing.maxQuantity = Math.max(existing.maxQuantity ?? -Infinity, summary.maxQuantity);
247
+ return existing;
248
+ }
249
+ const newSummary = {
250
+ id: `summary-${Date.now()}-${Math.random().toString(36).slice(2)}`,
251
+ ...summary
252
+ };
253
+ this.summaries.set(key, newSummary);
254
+ return newSummary;
255
+ }
256
+ async getMetric(key) {
257
+ return this.metrics.get(key) || null;
258
+ }
259
+ async listMetrics() {
260
+ return Array.from(this.metrics.values());
261
+ }
262
+ getSummaries() {
263
+ return Array.from(this.summaries.values());
264
+ }
265
+ clear() {
266
+ this.records = [];
267
+ this.summaries.clear();
268
+ this.metrics.clear();
269
+ }
270
+ };
271
+
272
+ //#endregion
273
+ export { InMemoryUsageStorage, UsageAggregator, formatPeriodKey, getPeriodEnd, getPeriodStart };
274
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["metricKey"],"sources":["../../src/aggregation/index.ts"],"sourcesContent":["/**\n * Usage aggregation engine.\n *\n * Provides periodic aggregation of usage records into summaries\n * for efficient billing and reporting queries.\n */\n\n// ============ Types ============\n\nexport type PeriodType = 'HOURLY' | 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'YEARLY';\nexport type AggregationType = 'COUNT' | 'SUM' | 'AVG' | 'MAX' | 'MIN' | 'LAST';\n\nexport interface UsageRecord {\n id: string;\n metricKey: string;\n subjectType: string;\n subjectId: string;\n quantity: number;\n timestamp: Date;\n}\n\nexport interface UsageSummary {\n id: string;\n metricKey: string;\n subjectType: string;\n subjectId: string;\n periodType: PeriodType;\n periodStart: Date;\n periodEnd: Date;\n totalQuantity: number;\n recordCount: number;\n minQuantity?: number;\n maxQuantity?: number;\n avgQuantity?: number;\n}\n\nexport interface MetricDefinition {\n key: string;\n aggregationType: AggregationType;\n}\n\nexport interface UsageStorage {\n /**\n * Get unaggregated records for a period.\n */\n getUnaggregatedRecords(options: {\n metricKey?: string;\n periodStart: Date;\n periodEnd: Date;\n limit?: number;\n }): Promise<UsageRecord[]>;\n\n /**\n * Mark records as aggregated.\n */\n markRecordsAggregated(recordIds: string[], aggregatedAt: Date): Promise<void>;\n\n /**\n * Get or create a summary record.\n */\n upsertSummary(summary: Omit<UsageSummary, 'id'>): Promise<UsageSummary>;\n\n /**\n * Get metric definition.\n */\n getMetric(key: string): Promise<MetricDefinition | null>;\n\n /**\n * List all active metrics.\n */\n listMetrics(): Promise<MetricDefinition[]>;\n}\n\nexport interface AggregationOptions {\n /** Storage implementation */\n storage: UsageStorage;\n /** Batch size for processing records */\n batchSize?: number;\n}\n\nexport interface AggregateParams {\n /** Period type to aggregate */\n periodType: PeriodType;\n /** Period start time */\n periodStart: Date;\n /** Period end time (optional, defaults to period boundary) */\n periodEnd?: Date;\n /** Specific metric to aggregate (optional, aggregates all if not specified) */\n metricKey?: string;\n}\n\nexport interface AggregationResult {\n periodType: PeriodType;\n periodStart: Date;\n periodEnd: Date;\n recordsProcessed: number;\n summariesCreated: number;\n summariesUpdated: number;\n errors: AggregationError[];\n}\n\nexport interface AggregationError {\n metricKey: string;\n subjectType: string;\n subjectId: string;\n error: string;\n}\n\n// ============ Period Helpers ============\n\n/**\n * Get the start of a period for a given date.\n */\nexport function getPeriodStart(date: Date, periodType: PeriodType): Date {\n const d = new Date(date);\n\n switch (periodType) {\n case 'HOURLY':\n d.setMinutes(0, 0, 0);\n return d;\n\n case 'DAILY':\n d.setHours(0, 0, 0, 0);\n return d;\n\n case 'WEEKLY': {\n d.setHours(0, 0, 0, 0);\n const day = d.getDay();\n d.setDate(d.getDate() - day);\n return d;\n }\n\n case 'MONTHLY':\n d.setHours(0, 0, 0, 0);\n d.setDate(1);\n return d;\n\n case 'YEARLY':\n d.setHours(0, 0, 0, 0);\n d.setMonth(0, 1);\n return d;\n }\n}\n\n/**\n * Get the end of a period for a given date.\n */\nexport function getPeriodEnd(date: Date, periodType: PeriodType): Date {\n const start = getPeriodStart(date, periodType);\n\n switch (periodType) {\n case 'HOURLY':\n return new Date(start.getTime() + 60 * 60 * 1000);\n\n case 'DAILY':\n return new Date(start.getTime() + 24 * 60 * 60 * 1000);\n\n case 'WEEKLY':\n return new Date(start.getTime() + 7 * 24 * 60 * 60 * 1000);\n\n case 'MONTHLY': {\n const end = new Date(start);\n end.setMonth(end.getMonth() + 1);\n return end;\n }\n\n case 'YEARLY': {\n const end = new Date(start);\n end.setFullYear(end.getFullYear() + 1);\n return end;\n }\n }\n}\n\n/**\n * Format a period key for grouping.\n */\nexport function formatPeriodKey(date: Date, periodType: PeriodType): string {\n const start = getPeriodStart(date, periodType);\n const year = start.getFullYear();\n const month = String(start.getMonth() + 1).padStart(2, '0');\n const day = String(start.getDate()).padStart(2, '0');\n const hour = String(start.getHours()).padStart(2, '0');\n\n switch (periodType) {\n case 'HOURLY':\n return `${year}-${month}-${day}T${hour}`;\n case 'DAILY':\n return `${year}-${month}-${day}`;\n case 'WEEKLY':\n return `${year}-W${getWeekNumber(start)}`;\n case 'MONTHLY':\n return `${year}-${month}`;\n case 'YEARLY':\n return `${year}`;\n }\n}\n\nfunction getWeekNumber(date: Date): string {\n const d = new Date(\n Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())\n );\n const dayNum = d.getUTCDay() || 7;\n d.setUTCDate(d.getUTCDate() + 4 - dayNum);\n const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));\n const weekNum = Math.ceil(\n ((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7\n );\n return String(weekNum).padStart(2, '0');\n}\n\n// ============ Aggregator ============\n\n/**\n * Usage aggregator.\n *\n * Aggregates usage records into summaries based on period type.\n */\nexport class UsageAggregator {\n private storage: UsageStorage;\n private batchSize: number;\n\n constructor(options: AggregationOptions) {\n this.storage = options.storage;\n this.batchSize = options.batchSize || 1000;\n }\n\n /**\n * Aggregate usage records for a period.\n */\n async aggregate(params: AggregateParams): Promise<AggregationResult> {\n const { periodType, periodStart, metricKey } = params;\n const periodEnd = params.periodEnd || getPeriodEnd(periodStart, periodType);\n\n const result: AggregationResult = {\n periodType,\n periodStart,\n periodEnd,\n recordsProcessed: 0,\n summariesCreated: 0,\n summariesUpdated: 0,\n errors: [],\n };\n\n // Get records to aggregate\n const records = await this.storage.getUnaggregatedRecords({\n metricKey,\n periodStart,\n periodEnd,\n limit: this.batchSize,\n });\n\n if (records.length === 0) {\n return result;\n }\n\n // Group records by metric, subject, and period\n const groups = this.groupRecords(records, periodType);\n\n // Process each group\n for (const [groupKey, groupRecords] of groups.entries()) {\n try {\n await this.aggregateGroup(groupKey, groupRecords, periodType, result);\n } catch (error) {\n const [metricKey, subjectType, subjectId] = groupKey.split('::');\n result.errors.push({\n metricKey: metricKey ?? 'unknown',\n subjectType: subjectType ?? 'unknown',\n subjectId: subjectId ?? 'unknown',\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n\n // Mark records as aggregated\n const recordIds = records.map((r) => r.id);\n await this.storage.markRecordsAggregated(recordIds, new Date());\n result.recordsProcessed = records.length;\n\n return result;\n }\n\n /**\n * Group records by metric, subject, and period.\n */\n private groupRecords(\n records: UsageRecord[],\n periodType: PeriodType\n ): Map<string, UsageRecord[]> {\n const groups = new Map<string, UsageRecord[]>();\n\n for (const record of records) {\n const periodKey = formatPeriodKey(record.timestamp, periodType);\n const groupKey = `${record.metricKey}::${record.subjectType}::${record.subjectId}::${periodKey}`;\n\n const existing = groups.get(groupKey) || [];\n existing.push(record);\n groups.set(groupKey, existing);\n }\n\n return groups;\n }\n\n /**\n * Aggregate a group of records into a summary.\n */\n private async aggregateGroup(\n groupKey: string,\n records: UsageRecord[],\n periodType: PeriodType,\n result: AggregationResult\n ): Promise<void> {\n const [metricKey, subjectType, subjectId] = groupKey.split('::');\n\n if (!metricKey || !subjectType || !subjectId || records.length === 0) {\n return;\n }\n\n const firstRecord = records[0];\n if (!firstRecord) return;\n const periodStart = getPeriodStart(firstRecord.timestamp, periodType);\n const periodEnd = getPeriodEnd(firstRecord.timestamp, periodType);\n\n // Get metric definition for aggregation type\n const metric = await this.storage.getMetric(metricKey);\n const aggregationType = metric?.aggregationType || 'SUM';\n\n // Calculate aggregated values\n const quantities = records.map((r) => r.quantity);\n const aggregated = this.calculateAggregation(quantities, aggregationType);\n\n // Create or update summary\n await this.storage.upsertSummary({\n metricKey,\n subjectType,\n subjectId,\n periodType,\n periodStart,\n periodEnd,\n totalQuantity: aggregated.total,\n recordCount: records.length,\n minQuantity: aggregated.min,\n maxQuantity: aggregated.max,\n avgQuantity: aggregated.avg,\n });\n\n result.summariesCreated++;\n }\n\n /**\n * Calculate aggregation values.\n */\n private calculateAggregation(\n quantities: number[],\n aggregationType: AggregationType\n ): { total: number; min: number; max: number; avg: number } {\n if (quantities.length === 0) {\n return { total: 0, min: 0, max: 0, avg: 0 };\n }\n\n const min = Math.min(...quantities);\n const max = Math.max(...quantities);\n const sum = quantities.reduce((a, b) => a + b, 0);\n const avg = sum / quantities.length;\n const count = quantities.length;\n\n let total: number;\n switch (aggregationType) {\n case 'COUNT':\n total = count;\n break;\n case 'SUM':\n total = sum;\n break;\n case 'AVG':\n total = avg;\n break;\n case 'MAX':\n total = max;\n break;\n case 'MIN':\n total = min;\n break;\n case 'LAST':\n total = quantities[quantities.length - 1] ?? 0;\n break;\n default:\n total = sum;\n }\n\n return { total, min, max, avg };\n }\n}\n\n// ============ In-Memory Storage ============\n\n/**\n * In-memory usage storage for testing.\n */\nexport class InMemoryUsageStorage implements UsageStorage {\n private records: UsageRecord[] = [];\n private summaries = new Map<string, UsageSummary>();\n private metrics = new Map<string, MetricDefinition>();\n\n addRecord(record: UsageRecord): void {\n this.records.push(record);\n }\n\n addMetric(metric: MetricDefinition): void {\n this.metrics.set(metric.key, metric);\n }\n\n async getUnaggregatedRecords(options: {\n metricKey?: string;\n periodStart: Date;\n periodEnd: Date;\n limit?: number;\n }): Promise<UsageRecord[]> {\n let records = this.records.filter((r) => {\n const inPeriod =\n r.timestamp >= options.periodStart && r.timestamp < options.periodEnd;\n const matchesMetric =\n !options.metricKey || r.metricKey === options.metricKey;\n return inPeriod && matchesMetric;\n });\n\n if (options.limit) {\n records = records.slice(0, options.limit);\n }\n\n return records;\n }\n\n async markRecordsAggregated(recordIds: string[]): Promise<void> {\n this.records = this.records.filter((r) => !recordIds.includes(r.id));\n }\n\n async upsertSummary(\n summary: Omit<UsageSummary, 'id'>\n ): Promise<UsageSummary> {\n const key = `${summary.metricKey}::${summary.subjectType}::${summary.subjectId}::${summary.periodType}::${summary.periodStart.toISOString()}`;\n\n const existing = this.summaries.get(key);\n if (existing) {\n // Update existing summary\n existing.totalQuantity += summary.totalQuantity;\n existing.recordCount += summary.recordCount;\n if (summary.minQuantity !== undefined) {\n existing.minQuantity = Math.min(\n existing.minQuantity ?? Infinity,\n summary.minQuantity\n );\n }\n if (summary.maxQuantity !== undefined) {\n existing.maxQuantity = Math.max(\n existing.maxQuantity ?? -Infinity,\n summary.maxQuantity\n );\n }\n return existing;\n }\n\n // Create new summary\n const newSummary: UsageSummary = {\n id: `summary-${Date.now()}-${Math.random().toString(36).slice(2)}`,\n ...summary,\n };\n this.summaries.set(key, newSummary);\n return newSummary;\n }\n\n async getMetric(key: string): Promise<MetricDefinition | null> {\n return this.metrics.get(key) || null;\n }\n\n async listMetrics(): Promise<MetricDefinition[]> {\n return Array.from(this.metrics.values());\n }\n\n getSummaries(): UsageSummary[] {\n return Array.from(this.summaries.values());\n }\n\n clear(): void {\n this.records = [];\n this.summaries.clear();\n this.metrics.clear();\n }\n}\n"],"mappings":";;;;AAiHA,SAAgB,eAAe,MAAY,YAA8B;CACvE,MAAM,IAAI,IAAI,KAAK,KAAK;AAExB,SAAQ,YAAR;EACE,KAAK;AACH,KAAE,WAAW,GAAG,GAAG,EAAE;AACrB,UAAO;EAET,KAAK;AACH,KAAE,SAAS,GAAG,GAAG,GAAG,EAAE;AACtB,UAAO;EAET,KAAK,UAAU;AACb,KAAE,SAAS,GAAG,GAAG,GAAG,EAAE;GACtB,MAAM,MAAM,EAAE,QAAQ;AACtB,KAAE,QAAQ,EAAE,SAAS,GAAG,IAAI;AAC5B,UAAO;;EAGT,KAAK;AACH,KAAE,SAAS,GAAG,GAAG,GAAG,EAAE;AACtB,KAAE,QAAQ,EAAE;AACZ,UAAO;EAET,KAAK;AACH,KAAE,SAAS,GAAG,GAAG,GAAG,EAAE;AACtB,KAAE,SAAS,GAAG,EAAE;AAChB,UAAO;;;;;;AAOb,SAAgB,aAAa,MAAY,YAA8B;CACrE,MAAM,QAAQ,eAAe,MAAM,WAAW;AAE9C,SAAQ,YAAR;EACE,KAAK,SACH,QAAO,IAAI,KAAK,MAAM,SAAS,GAAG,OAAU,IAAK;EAEnD,KAAK,QACH,QAAO,IAAI,KAAK,MAAM,SAAS,GAAG,OAAU,KAAK,IAAK;EAExD,KAAK,SACH,QAAO,IAAI,KAAK,MAAM,SAAS,GAAG,QAAc,KAAK,IAAK;EAE5D,KAAK,WAAW;GACd,MAAM,MAAM,IAAI,KAAK,MAAM;AAC3B,OAAI,SAAS,IAAI,UAAU,GAAG,EAAE;AAChC,UAAO;;EAGT,KAAK,UAAU;GACb,MAAM,MAAM,IAAI,KAAK,MAAM;AAC3B,OAAI,YAAY,IAAI,aAAa,GAAG,EAAE;AACtC,UAAO;;;;;;;AAQb,SAAgB,gBAAgB,MAAY,YAAgC;CAC1E,MAAM,QAAQ,eAAe,MAAM,WAAW;CAC9C,MAAM,OAAO,MAAM,aAAa;CAChC,MAAM,QAAQ,OAAO,MAAM,UAAU,GAAG,EAAE,CAAC,SAAS,GAAG,IAAI;CAC3D,MAAM,MAAM,OAAO,MAAM,SAAS,CAAC,CAAC,SAAS,GAAG,IAAI;CACpD,MAAM,OAAO,OAAO,MAAM,UAAU,CAAC,CAAC,SAAS,GAAG,IAAI;AAEtD,SAAQ,YAAR;EACE,KAAK,SACH,QAAO,GAAG,KAAK,GAAG,MAAM,GAAG,IAAI,GAAG;EACpC,KAAK,QACH,QAAO,GAAG,KAAK,GAAG,MAAM,GAAG;EAC7B,KAAK,SACH,QAAO,GAAG,KAAK,IAAI,cAAc,MAAM;EACzC,KAAK,UACH,QAAO,GAAG,KAAK,GAAG;EACpB,KAAK,SACH,QAAO,GAAG;;;AAIhB,SAAS,cAAc,MAAoB;CACzC,MAAM,IAAI,IAAI,KACZ,KAAK,IAAI,KAAK,aAAa,EAAE,KAAK,UAAU,EAAE,KAAK,SAAS,CAAC,CAC9D;CACD,MAAM,SAAS,EAAE,WAAW,IAAI;AAChC,GAAE,WAAW,EAAE,YAAY,GAAG,IAAI,OAAO;CACzC,MAAM,YAAY,IAAI,KAAK,KAAK,IAAI,EAAE,gBAAgB,EAAE,GAAG,EAAE,CAAC;CAC9D,MAAM,UAAU,KAAK,OACjB,EAAE,SAAS,GAAG,UAAU,SAAS,IAAI,QAAW,KAAK,EACxD;AACD,QAAO,OAAO,QAAQ,CAAC,SAAS,GAAG,IAAI;;;;;;;AAUzC,IAAa,kBAAb,MAA6B;CAC3B,AAAQ;CACR,AAAQ;CAER,YAAY,SAA6B;AACvC,OAAK,UAAU,QAAQ;AACvB,OAAK,YAAY,QAAQ,aAAa;;;;;CAMxC,MAAM,UAAU,QAAqD;EACnE,MAAM,EAAE,YAAY,aAAa,cAAc;EAC/C,MAAM,YAAY,OAAO,aAAa,aAAa,aAAa,WAAW;EAE3E,MAAM,SAA4B;GAChC;GACA;GACA;GACA,kBAAkB;GAClB,kBAAkB;GAClB,kBAAkB;GAClB,QAAQ,EAAE;GACX;EAGD,MAAM,UAAU,MAAM,KAAK,QAAQ,uBAAuB;GACxD;GACA;GACA;GACA,OAAO,KAAK;GACb,CAAC;AAEF,MAAI,QAAQ,WAAW,EACrB,QAAO;EAIT,MAAM,SAAS,KAAK,aAAa,SAAS,WAAW;AAGrD,OAAK,MAAM,CAAC,UAAU,iBAAiB,OAAO,SAAS,CACrD,KAAI;AACF,SAAM,KAAK,eAAe,UAAU,cAAc,YAAY,OAAO;WAC9D,OAAO;GACd,MAAM,CAACA,aAAW,aAAa,aAAa,SAAS,MAAM,KAAK;AAChE,UAAO,OAAO,KAAK;IACjB,WAAWA,eAAa;IACxB,aAAa,eAAe;IAC5B,WAAW,aAAa;IACxB,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;IAC9D,CAAC;;EAKN,MAAM,YAAY,QAAQ,KAAK,MAAM,EAAE,GAAG;AAC1C,QAAM,KAAK,QAAQ,sBAAsB,2BAAW,IAAI,MAAM,CAAC;AAC/D,SAAO,mBAAmB,QAAQ;AAElC,SAAO;;;;;CAMT,AAAQ,aACN,SACA,YAC4B;EAC5B,MAAM,yBAAS,IAAI,KAA4B;AAE/C,OAAK,MAAM,UAAU,SAAS;GAC5B,MAAM,YAAY,gBAAgB,OAAO,WAAW,WAAW;GAC/D,MAAM,WAAW,GAAG,OAAO,UAAU,IAAI,OAAO,YAAY,IAAI,OAAO,UAAU,IAAI;GAErF,MAAM,WAAW,OAAO,IAAI,SAAS,IAAI,EAAE;AAC3C,YAAS,KAAK,OAAO;AACrB,UAAO,IAAI,UAAU,SAAS;;AAGhC,SAAO;;;;;CAMT,MAAc,eACZ,UACA,SACA,YACA,QACe;EACf,MAAM,CAAC,WAAW,aAAa,aAAa,SAAS,MAAM,KAAK;AAEhE,MAAI,CAAC,aAAa,CAAC,eAAe,CAAC,aAAa,QAAQ,WAAW,EACjE;EAGF,MAAM,cAAc,QAAQ;AAC5B,MAAI,CAAC,YAAa;EAClB,MAAM,cAAc,eAAe,YAAY,WAAW,WAAW;EACrE,MAAM,YAAY,aAAa,YAAY,WAAW,WAAW;EAIjE,MAAM,mBADS,MAAM,KAAK,QAAQ,UAAU,UAAU,GACtB,mBAAmB;EAGnD,MAAM,aAAa,QAAQ,KAAK,MAAM,EAAE,SAAS;EACjD,MAAM,aAAa,KAAK,qBAAqB,YAAY,gBAAgB;AAGzE,QAAM,KAAK,QAAQ,cAAc;GAC/B;GACA;GACA;GACA;GACA;GACA;GACA,eAAe,WAAW;GAC1B,aAAa,QAAQ;GACrB,aAAa,WAAW;GACxB,aAAa,WAAW;GACxB,aAAa,WAAW;GACzB,CAAC;AAEF,SAAO;;;;;CAMT,AAAQ,qBACN,YACA,iBAC0D;AAC1D,MAAI,WAAW,WAAW,EACxB,QAAO;GAAE,OAAO;GAAG,KAAK;GAAG,KAAK;GAAG,KAAK;GAAG;EAG7C,MAAM,MAAM,KAAK,IAAI,GAAG,WAAW;EACnC,MAAM,MAAM,KAAK,IAAI,GAAG,WAAW;EACnC,MAAM,MAAM,WAAW,QAAQ,GAAG,MAAM,IAAI,GAAG,EAAE;EACjD,MAAM,MAAM,MAAM,WAAW;EAC7B,MAAM,QAAQ,WAAW;EAEzB,IAAI;AACJ,UAAQ,iBAAR;GACE,KAAK;AACH,YAAQ;AACR;GACF,KAAK;AACH,YAAQ;AACR;GACF,KAAK;AACH,YAAQ;AACR;GACF,KAAK;AACH,YAAQ;AACR;GACF,KAAK;AACH,YAAQ;AACR;GACF,KAAK;AACH,YAAQ,WAAW,WAAW,SAAS,MAAM;AAC7C;GACF,QACE,SAAQ;;AAGZ,SAAO;GAAE;GAAO;GAAK;GAAK;GAAK;;;;;;AASnC,IAAa,uBAAb,MAA0D;CACxD,AAAQ,UAAyB,EAAE;CACnC,AAAQ,4BAAY,IAAI,KAA2B;CACnD,AAAQ,0BAAU,IAAI,KAA+B;CAErD,UAAU,QAA2B;AACnC,OAAK,QAAQ,KAAK,OAAO;;CAG3B,UAAU,QAAgC;AACxC,OAAK,QAAQ,IAAI,OAAO,KAAK,OAAO;;CAGtC,MAAM,uBAAuB,SAKF;EACzB,IAAI,UAAU,KAAK,QAAQ,QAAQ,MAAM;GACvC,MAAM,WACJ,EAAE,aAAa,QAAQ,eAAe,EAAE,YAAY,QAAQ;GAC9D,MAAM,gBACJ,CAAC,QAAQ,aAAa,EAAE,cAAc,QAAQ;AAChD,UAAO,YAAY;IACnB;AAEF,MAAI,QAAQ,MACV,WAAU,QAAQ,MAAM,GAAG,QAAQ,MAAM;AAG3C,SAAO;;CAGT,MAAM,sBAAsB,WAAoC;AAC9D,OAAK,UAAU,KAAK,QAAQ,QAAQ,MAAM,CAAC,UAAU,SAAS,EAAE,GAAG,CAAC;;CAGtE,MAAM,cACJ,SACuB;EACvB,MAAM,MAAM,GAAG,QAAQ,UAAU,IAAI,QAAQ,YAAY,IAAI,QAAQ,UAAU,IAAI,QAAQ,WAAW,IAAI,QAAQ,YAAY,aAAa;EAE3I,MAAM,WAAW,KAAK,UAAU,IAAI,IAAI;AACxC,MAAI,UAAU;AAEZ,YAAS,iBAAiB,QAAQ;AAClC,YAAS,eAAe,QAAQ;AAChC,OAAI,QAAQ,gBAAgB,OAC1B,UAAS,cAAc,KAAK,IAC1B,SAAS,eAAe,UACxB,QAAQ,YACT;AAEH,OAAI,QAAQ,gBAAgB,OAC1B,UAAS,cAAc,KAAK,IAC1B,SAAS,eAAe,WACxB,QAAQ,YACT;AAEH,UAAO;;EAIT,MAAM,aAA2B;GAC/B,IAAI,WAAW,KAAK,KAAK,CAAC,GAAG,KAAK,QAAQ,CAAC,SAAS,GAAG,CAAC,MAAM,EAAE;GAChE,GAAG;GACJ;AACD,OAAK,UAAU,IAAI,KAAK,WAAW;AACnC,SAAO;;CAGT,MAAM,UAAU,KAA+C;AAC7D,SAAO,KAAK,QAAQ,IAAI,IAAI,IAAI;;CAGlC,MAAM,cAA2C;AAC/C,SAAO,MAAM,KAAK,KAAK,QAAQ,QAAQ,CAAC;;CAG1C,eAA+B;AAC7B,SAAO,MAAM,KAAK,KAAK,UAAU,QAAQ,CAAC;;CAG5C,QAAc;AACZ,OAAK,UAAU,EAAE;AACjB,OAAK,UAAU,OAAO;AACtB,OAAK,QAAQ,OAAO"}