@lssm/lib.metering 1.41.1 → 1.42.2

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 CHANGED
@@ -1,5 +1,8 @@
1
1
  # @lssm/lib.metering
2
2
 
3
+ Website: https://contractspec.lssm.tech/
4
+
5
+
3
6
  Usage metering and billing core module for ContractSpec applications.
4
7
 
5
8
  ## Overview
@@ -183,3 +186,13 @@ export const schemaComposition = {
183
186
 
184
187
 
185
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"}
@@ -1 +1,274 @@
1
- function e(e,t){let n=new Date(e);switch(t){case`HOURLY`:return n.setMinutes(0,0,0),n;case`DAILY`:return n.setHours(0,0,0,0),n;case`WEEKLY`:n.setHours(0,0,0,0);let e=n.getDay();return n.setDate(n.getDate()-e),n;case`MONTHLY`:return n.setHours(0,0,0,0),n.setDate(1),n;case`YEARLY`:return n.setHours(0,0,0,0),n.setMonth(0,1),n}}function t(t,n){let r=e(t,n);switch(n){case`HOURLY`:return new Date(r.getTime()+3600*1e3);case`DAILY`:return new Date(r.getTime()+1440*60*1e3);case`WEEKLY`:return new Date(r.getTime()+10080*60*1e3);case`MONTHLY`:{let e=new Date(r);return e.setMonth(e.getMonth()+1),e}case`YEARLY`:{let e=new Date(r);return e.setFullYear(e.getFullYear()+1),e}}}function n(t,n){let i=e(t,n),a=i.getFullYear(),o=String(i.getMonth()+1).padStart(2,`0`),s=String(i.getDate()).padStart(2,`0`),c=String(i.getHours()).padStart(2,`0`);switch(n){case`HOURLY`:return`${a}-${o}-${s}T${c}`;case`DAILY`:return`${a}-${o}-${s}`;case`WEEKLY`:return`${a}-W${r(i)}`;case`MONTHLY`:return`${a}-${o}`;case`YEARLY`:return`${a}`}}function r(e){let t=new Date(Date.UTC(e.getFullYear(),e.getMonth(),e.getDate())),n=t.getUTCDay()||7;t.setUTCDate(t.getUTCDate()+4-n);let r=new Date(Date.UTC(t.getUTCFullYear(),0,1)),i=Math.ceil(((t.getTime()-r.getTime())/864e5+1)/7);return String(i).padStart(2,`0`)}var i=class{storage;batchSize;constructor(e){this.storage=e.storage,this.batchSize=e.batchSize||1e3}async aggregate(e){let{periodType:n,periodStart:r,metricKey:i}=e,a=e.periodEnd||t(r,n),o={periodType:n,periodStart:r,periodEnd:a,recordsProcessed:0,summariesCreated:0,summariesUpdated:0,errors:[]},s=await this.storage.getUnaggregatedRecords({metricKey:i,periodStart:r,periodEnd:a,limit:this.batchSize});if(s.length===0)return o;let c=this.groupRecords(s,n);for(let[e,t]of c.entries())try{await this.aggregateGroup(e,t,n,o)}catch(t){let[n,r,i]=e.split(`::`);o.errors.push({metricKey:n,subjectType:r,subjectId:i,error:t instanceof Error?t.message:String(t)})}let l=s.map(e=>e.id);return await this.storage.markRecordsAggregated(l,new Date),o.recordsProcessed=s.length,o}groupRecords(e,t){let r=new Map;for(let i of e){let e=n(i.timestamp,t),a=`${i.metricKey}::${i.subjectType}::${i.subjectId}::${e}`,o=r.get(a)||[];o.push(i),r.set(a,o)}return r}async aggregateGroup(n,r,i,a){let[o,s,c]=n.split(`::`);if(!o||!s||!c||r.length===0)return;let l=r[0],u=e(l.timestamp,i),d=t(l.timestamp,i),f=(await this.storage.getMetric(o))?.aggregationType||`SUM`,p=r.map(e=>e.quantity),m=this.calculateAggregation(p,f);await this.storage.upsertSummary({metricKey:o,subjectType:s,subjectId:c,periodType:i,periodStart:u,periodEnd:d,totalQuantity:m.total,recordCount:r.length,minQuantity:m.min,maxQuantity:m.max,avgQuantity:m.avg}),a.summariesCreated++}calculateAggregation(e,t){if(e.length===0)return{total:0,min:0,max:0,avg:0};let n=Math.min(...e),r=Math.max(...e),i=e.reduce((e,t)=>e+t,0),a=i/e.length,o=e.length,s;switch(t){case`COUNT`:s=o;break;case`SUM`:s=i;break;case`AVG`:s=a;break;case`MAX`:s=r;break;case`MIN`:s=n;break;case`LAST`:s=e[e.length-1]??0;break;default:s=i}return{total:s,min:n,max:r,avg:a}}},a=class{records=[];summaries=new Map;metrics=new Map;addRecord(e){this.records.push(e)}addMetric(e){this.metrics.set(e.key,e)}async getUnaggregatedRecords(e){let t=this.records.filter(t=>{let n=t.timestamp>=e.periodStart&&t.timestamp<e.periodEnd,r=!e.metricKey||t.metricKey===e.metricKey;return n&&r});return e.limit&&(t=t.slice(0,e.limit)),t}async markRecordsAggregated(e){this.records=this.records.filter(t=>!e.includes(t.id))}async upsertSummary(e){let t=`${e.metricKey}::${e.subjectType}::${e.subjectId}::${e.periodType}::${e.periodStart.toISOString()}`,n=this.summaries.get(t);if(n)return n.totalQuantity+=e.totalQuantity,n.recordCount+=e.recordCount,e.minQuantity!==void 0&&(n.minQuantity=Math.min(n.minQuantity??1/0,e.minQuantity)),e.maxQuantity!==void 0&&(n.maxQuantity=Math.max(n.maxQuantity??-1/0,e.maxQuantity)),n;let r={id:`summary-${Date.now()}-${Math.random().toString(36).slice(2)}`,...e};return this.summaries.set(t,r),r}async getMetric(e){return this.metrics.get(e)||null}async listMetrics(){return Array.from(this.metrics.values())}getSummaries(){return Array.from(this.summaries.values())}clear(){this.records=[],this.summaries.clear(),this.metrics.clear()}};export{a as InMemoryUsageStorage,i as UsageAggregator,n as formatPeriodKey,t as getPeriodEnd,e as getPeriodStart};
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":["result: AggregationResult","metricKey","total: number","newSummary: UsageSummary"],"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":"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"}