Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).


9 changes: 9 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

170 changes: 170 additions & 0 deletions src/module49.ts
Original file line number Diff line number Diff line change
@@ -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<string, { withdrawable: bigint; progress: number; computedAt: number }>();
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,
};
}
}
179 changes: 179 additions & 0 deletions src/tests/module49.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});