@contractspec/lib.metering 1.46.2 → 1.47.0
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/aggregation/index.js.map +1 -1
- package/dist/contracts/index.d.ts +291 -291
- package/dist/entities/index.d.ts +185 -185
- package/dist/entities/index.d.ts.map +1 -1
- package/dist/entities/index.js.map +1 -1
- package/dist/events.d.ts +135 -135
- package/dist/events.d.ts.map +1 -1
- package/dist/metering.capability.d.ts +8 -0
- package/dist/metering.capability.d.ts.map +1 -0
- package/dist/metering.capability.js +33 -0
- package/dist/metering.capability.js.map +1 -0
- package/dist/metering.feature.d.ts +4 -4
- package/dist/metering.feature.d.ts.map +1 -1
- package/dist/metering.feature.js +11 -4
- package/dist/metering.feature.js.map +1 -1
- package/package.json +7 -8
|
@@ -1 +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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"}
|
|
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":"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"}
|