diff --git a/docs/api.md b/docs/api.md index 9b65ffa..1613894 100644 --- a/docs/api.md +++ b/docs/api.md @@ -758,3 +758,30 @@ new Module48(config?: Module48Config) * `clearCache(): void` - Clears the internal lookup cache and metrics. * `getPerformanceMetrics(): Module48Metrics` - Returns `totalProcessed`, `cacheHits`, `cacheMisses`, `averageExecutionTimeMs`, and `measuredSpeedupPercent` (a real measurement derived from this instance's own accumulated hit/miss timings, `null` until both have occurred at least once — not a fixed assumed percentage). +--- + +## `Module49` (Feature #49) + +Streaming analytics and evaluation engine implementing Feature #49. Built with memoized cache algorithms and pre-allocated buffer iteration for fast stream batch evaluation. + +### Constructor + +```typescript +new Module49(config?: Module49Config) +``` + +| Option | Type | Default | Notes | +|--------|------|---------|-------| +| `cacheSize` | `number` | `1000` | Max entries in memoization cache | +| `enableOptimization` | `boolean` | `true` | Enables memoized lookup caching | +| `batchChunkSize` | `number` | `50` | Stream chunk size for batch processing | + +### Methods + +* `processSingleItem(item: StreamBatchItem49): Module49Result` - Evaluates a stream's withdrawable balance and progress. +* `processStreamBatch(items: StreamBatchItem49[]): Module49Result[]` - Processes batch array of streams in chunks. +* `computeOptimizedYield(ratePerSecond: bigint, durationSecs: number): bigint` - Fast BigInt yield calculation. +* `clearCache(): void` - Clears the internal lookup cache and metrics. +* `getPerformanceMetrics(): Module49Metrics` - Returns real-time metrics (`totalProcessed`, `cacheHits`, `cacheMisses`, `hitRate`, `averageExecutionTimeMs`). + + diff --git a/src/index.ts b/src/index.ts index ba74de3..c3d6898 100644 --- a/src/index.ts +++ b/src/index.ts @@ -83,3 +83,12 @@ export type { Module48Result, Module48Metrics, } from './module48.js'; + +export { Module49 } from './module49.js'; +export type { + Module49Config, + StreamBatchItem49, + Module49Result, + Module49Metrics, +} from './module49.js'; + diff --git a/src/module49.ts b/src/module49.ts new file mode 100644 index 0000000..5e0fd03 --- /dev/null +++ b/src/module49.ts @@ -0,0 +1,170 @@ +import type { StreamInfo } from './types/index.js'; +import { withdrawableLocal } from './utils.js'; + +export interface Module49Config { + /** Maximum number of calculated results to keep in the fast lookup cache */ + cacheSize?: number; + /** Enable performance optimization via memoization and pre-allocated buffer processing */ + enableOptimization?: boolean; + /** Preferred chunk size for batch processing stream items */ + batchChunkSize?: number; +} + +export interface StreamBatchItem49 { + id: string; + stream: StreamInfo; + timestamp?: number; +} + +export interface Module49Result { + id: string; + withdrawable: bigint; + progress: number; + isCached: boolean; + computedAt: number; +} + +export interface Module49Metrics { + totalProcessed: number; + cacheHits: number; + cacheMisses: number; + hitRate: number; + averageExecutionTimeMs: number; +} + +/** + * Module 49: High-Performance SDK Streaming Analytics Engine + * + * Implements Feature #49 with memoized calculation algorithms for batch stream evaluation. + */ +export class Module49 { + private readonly cacheSize: number; + private readonly enableOptimization: boolean; + private readonly batchChunkSize: number; + + private cache = new Map(); + private totalProcessed = 0; + private cacheHits = 0; + private cacheMisses = 0; + private totalExecutionTimeMs = 0; + + constructor(config: Module49Config = {}) { + this.cacheSize = config.cacheSize ?? 1000; + this.enableOptimization = config.enableOptimization ?? true; + this.batchChunkSize = config.batchChunkSize ?? 50; + } + + /** + * Process a list of streams using optimized batch evaluation algorithms. + */ + public processStreamBatch(items: StreamBatchItem49[]): Module49Result[] { + const startTime = performance.now(); + const results: Module49Result[] = new Array(items.length); + + for (let i = 0; i < items.length; i += this.batchChunkSize) { + const chunkEnd = Math.min(i + this.batchChunkSize, items.length); + for (let j = i; j < chunkEnd; j++) { + const item = items[j]; + if (!item) continue; + + results[j] = this.processSingleItem(item); + } + } + + const elapsed = performance.now() - startTime; + this.totalExecutionTimeMs += elapsed; + this.totalProcessed += items.length; + + return results; + } + + /** + * Evaluate a single stream item with fast-path cache lookup. + */ + public processSingleItem(item: StreamBatchItem49): Module49Result { + const nowSec = item.timestamp ?? Math.floor(Date.now() / 1000); + const cacheKey = `${item.id}_${item.stream.withdrawn.toString()}_${item.stream.paused ? 1 : 0}_${nowSec}`; + + if (this.enableOptimization && this.cache.has(cacheKey)) { + this.cacheHits++; + const cached = this.cache.get(cacheKey)!; + return { + id: item.id, + withdrawable: cached.withdrawable, + progress: cached.progress, + isCached: true, + computedAt: cached.computedAt, + }; + } + + this.cacheMisses++; + const withdrawable = withdrawableLocal(item.stream, nowSec); + + let progress = 0; + const { startTime, endTime } = item.stream; + if (nowSec >= startTime) { + if (endTime === 0) { + progress = 0.5; // open-ended active + } else if (nowSec >= endTime) { + progress = 1.0; + } else { + progress = (nowSec - startTime) / (endTime - startTime); + } + } + + const computedAt = nowSec; + + if (this.enableOptimization) { + if (this.cache.size >= this.cacheSize) { + const firstKey = this.cache.keys().next().value; + if (firstKey !== undefined) { + this.cache.delete(firstKey); + } + } + this.cache.set(cacheKey, { withdrawable, progress, computedAt }); + } + + return { + id: item.id, + withdrawable, + progress, + isCached: false, + computedAt, + }; + } + + /** + * High-performance fast calculation of stream yields over arbitrary durations. + */ + public computeOptimizedYield(ratePerSecond: bigint, durationSecs: number): bigint { + if (durationSecs <= 0 || ratePerSecond <= 0n) return 0n; + return ratePerSecond * BigInt(durationSecs); + } + + /** + * Reset performance cache and internal state. + */ + public clearCache(): void { + this.cache.clear(); + this.cacheHits = 0; + this.cacheMisses = 0; + this.totalProcessed = 0; + this.totalExecutionTimeMs = 0; + } + + /** + * Retrieve performance metrics including hit rate and raw lookup counters. + */ + public getPerformanceMetrics(): Module49Metrics { + const totalRequests = this.cacheHits + this.cacheMisses; + const hitRate = totalRequests > 0 ? this.cacheHits / totalRequests : 0; + + return { + totalProcessed: this.totalProcessed, + cacheHits: this.cacheHits, + cacheMisses: this.cacheMisses, + hitRate: this.enableOptimization ? hitRate : 0, + averageExecutionTimeMs: this.totalProcessed > 0 ? this.totalExecutionTimeMs / this.totalProcessed : 0, + }; + } +} diff --git a/src/tests/module49.test.ts b/src/tests/module49.test.ts new file mode 100644 index 0000000..7af800d --- /dev/null +++ b/src/tests/module49.test.ts @@ -0,0 +1,179 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { Module49 } from '../module49.js'; +import type { StreamInfo } from '../types/index.js'; + +describe('Module49 (SDK Feature #49)', () => { + let module49: Module49; + const now = 1000; + + const mockStream: StreamInfo = { + id: 1n, + sender: 'GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFXYCZLYC3ZCHB2D4P3CF', + recipient: 'GA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVSGZ', + token: 'native', + depositAmount: 1000000000n, + ratePerSecond: 100n, + startTime: 500, + endTime: 1500, + withdrawn: 0n, + paused: false, + pausedAt: 0, + cancelled: false, + clawbackEnabled: false, + }; + + beforeEach(() => { + module49 = new Module49({ cacheSize: 10, batchChunkSize: 5 }); + }); + + describe('Constructor & Configuration', () => { + it('initializes with default options', () => { + const defaultMod = new Module49(); + const metrics = defaultMod.getPerformanceMetrics(); + expect(metrics.totalProcessed).toBe(0); + expect(metrics.hitRate).toBe(0); + }); + + it('initializes with custom options', () => { + const customMod = new Module49({ + cacheSize: 50, + enableOptimization: false, + batchChunkSize: 10, + }); + const metrics = customMod.getPerformanceMetrics(); + expect(metrics.hitRate).toBe(0); + }); + }); + + describe('processSingleItem', () => { + it('calculates withdrawable balance and progress for active stream', () => { + const item = { id: 'stream-1', stream: mockStream, timestamp: now }; + const result = module49.processSingleItem(item); + + expect(result.id).toBe('stream-1'); + expect(result.withdrawable).toBe(50000n); // (1000 - 500) * 100 + expect(result.progress).toBe(0.5); // (1000 - 500) / (1500 - 500) + expect(result.isCached).toBe(false); + expect(result.computedAt).toBe(now); + }); + + it('handles stream prior to start time', () => { + const item = { id: 'stream-future', stream: mockStream, timestamp: 400 }; + const result = module49.processSingleItem(item); + + expect(result.withdrawable).toBe(0n); + expect(result.progress).toBe(0); + }); + + it('handles completed stream after end time', () => { + const item = { id: 'stream-past', stream: mockStream, timestamp: 2000 }; + const result = module49.processSingleItem(item); + + expect(result.progress).toBe(1.0); + }); + + it('handles open-ended stream with no end time', () => { + const openStream: StreamInfo = { ...mockStream, endTime: 0 }; + const item = { id: 'stream-open', stream: openStream, timestamp: now }; + const result = module49.processSingleItem(item); + + expect(result.progress).toBe(0.5); + }); + + it('uses system current time if timestamp is omitted', () => { + const item = { id: 'stream-now', stream: mockStream }; + const result = module49.processSingleItem(item); + expect(result.computedAt).toBeGreaterThan(0); + }); + }); + + describe('Optimization & Caching', () => { + it('serves cached results on duplicate evaluation requests', () => { + const item = { id: 'stream-1', stream: mockStream, timestamp: now }; + + const firstPass = module49.processSingleItem(item); + expect(firstPass.isCached).toBe(false); + + const secondPass = module49.processSingleItem(item); + expect(secondPass.isCached).toBe(true); + expect(secondPass.withdrawable).toBe(firstPass.withdrawable); + + const metrics = module49.getPerformanceMetrics(); + expect(metrics.cacheHits).toBe(1); + expect(metrics.cacheMisses).toBe(1); + expect(metrics.hitRate).toBe(0.5); + }); + + it('evicts oldest cache item when cacheSize threshold is reached', () => { + const smallCacheMod = new Module49({ cacheSize: 2 }); + + smallCacheMod.processSingleItem({ id: 'item-1', stream: mockStream, timestamp: 1000 }); + smallCacheMod.processSingleItem({ id: 'item-2', stream: mockStream, timestamp: 1001 }); + smallCacheMod.processSingleItem({ id: 'item-3', stream: mockStream, timestamp: 1002 }); + + // item-1 should be evicted + const reQuery = smallCacheMod.processSingleItem({ id: 'item-1', stream: mockStream, timestamp: 1000 }); + expect(reQuery.isCached).toBe(false); + }); + + it('bypasses cache when optimization is disabled', () => { + const unoptimizedMod = new Module49({ enableOptimization: false }); + const item = { id: 'stream-1', stream: mockStream, timestamp: now }; + + unoptimizedMod.processSingleItem(item); + const secondPass = unoptimizedMod.processSingleItem(item); + + expect(secondPass.isCached).toBe(false); + expect(unoptimizedMod.getPerformanceMetrics().hitRate).toBe(0); + }); + }); + + describe('processStreamBatch', () => { + it('processes batch of stream items in chunked iterations', () => { + const batchItems = Array.from({ length: 12 }, (_, i) => ({ + id: `stream-${i}`, + stream: { ...mockStream, id: BigInt(i) }, + timestamp: now, + })); + + const results = module49.processStreamBatch(batchItems); + + expect(results).toHaveLength(12); + expect(results[0]?.id).toBe('stream-0'); + expect(results[11]?.id).toBe('stream-11'); + + const metrics = module49.getPerformanceMetrics(); + expect(metrics.totalProcessed).toBe(12); + }); + }); + + describe('computeOptimizedYield', () => { + it('computes yield accurately with BigInt precision', () => { + const yieldResult = module49.computeOptimizedYield(100n, 3600); + expect(yieldResult).toBe(360000n); + }); + + it('returns 0n for non-positive input values', () => { + expect(module49.computeOptimizedYield(0n, 3600)).toBe(0n); + expect(module49.computeOptimizedYield(100n, 0)).toBe(0n); + expect(module49.computeOptimizedYield(100n, -10)).toBe(0n); + }); + }); + + describe('clearCache & Metrics', () => { + it('resets cache state and performance counters', () => { + const item = { id: 'stream-1', stream: mockStream, timestamp: now }; + module49.processSingleItem(item); + module49.processSingleItem(item); + + expect(module49.getPerformanceMetrics().cacheHits).toBe(1); + + module49.clearCache(); + + const freshMetrics = module49.getPerformanceMetrics(); + expect(freshMetrics.cacheHits).toBe(0); + expect(freshMetrics.cacheMisses).toBe(0); + expect(freshMetrics.totalProcessed).toBe(0); + }); + }); +});