feat(stdlib): new modules + eslint/tsconfig migration

- Add array/async/etc. modules and type tests; migrate to eslint flat config
  and composite tsconfig (vitest typecheck enabled).
- Fix PubSub.emit to snapshot listeners before iterating (stable EventEmitter
  semantics; avoids invoking listeners added during the same emit).
This commit is contained in:
2026-06-07 16:29:08 +07:00
parent 008d85a8fd
commit 96f4cba4a8
118 changed files with 3511 additions and 240 deletions
@@ -1,4 +1,4 @@
import { describe, it, expect } from 'vitest';
import { describe, expect, it } from 'vitest';
import { clamp } from '.';
describe('clamp', () => {
+11 -1
View File
@@ -1,4 +1,4 @@
import { describe, it, expect } from 'vitest';
import { describe, expect, it } from 'vitest';
import { inverseLerp, lerp } from '.';
describe('lerp', () => {
@@ -26,6 +26,16 @@ describe('lerp', () => {
const result = lerp(0, 10, 1.5);
expect(result).toBe(15);
});
it('interpolates from a non-zero start', () => {
expect(lerp(10, 20, 0.5)).toBe(15);
expect(lerp(-10, 10, 0.25)).toBe(-5);
});
it('propagates NaN and Infinity', () => {
expect(lerp(0, 10, Number.NaN)).toBeNaN();
expect(lerp(0, Number.POSITIVE_INFINITY, 0.5)).toBe(Number.POSITIVE_INFINITY);
});
});
describe('inverseLerp', () => {
@@ -43,4 +43,12 @@ describe('remap', () => {
// input range is zero (should return output min)
expect(remap(5, 0, 0, 0, 100)).toBe(0);
});
it('handle a reversed (descending) input range', () => {
// 2 is 80% of the way from 10 down to 0
expect(remap(2, 10, 0, 0, 100)).toBe(80);
// clamps to the interval regardless of orientation
expect(remap(15, 10, 0, 0, 100)).toBe(0);
expect(remap(-5, 10, 0, 0, 100)).toBe(100);
});
});
+3 -1
View File
@@ -19,7 +19,9 @@ export function remap(value: number, in_min: number, in_max: number, out_min: nu
if (in_min === in_max)
return out_min;
const clampedValue = clamp(value, in_min, in_max);
// Clamp to the interval's actual bounds so a reversed (descending) input range still works;
// inverseLerp itself handles in_min > in_max correctly.
const clampedValue = clamp(value, Math.min(in_min, in_max), Math.max(in_min, in_max));
return lerp(out_min, out_max, inverseLerp(in_min, in_max, clampedValue));
}
@@ -1,4 +1,4 @@
import { describe, it, expect } from 'vitest';
import { describe, expect, it } from 'vitest';
import { clampBigInt } from '.';
describe('clampBigInt', () => {
@@ -1,4 +1,4 @@
import { describe, it, expect } from 'vitest';
import { describe, expect, it } from 'vitest';
import { inverseLerpBigInt, lerpBigInt } from '.';
const MAX_SAFE_INTEGER = BigInt(Number.MAX_SAFE_INTEGER);
@@ -28,6 +28,20 @@ describe('lerpBigInt', () => {
const result = lerpBigInt(0n, 10n, 1.5);
expect(result).toBe(15n);
});
it('truncates the fractional part toward zero', () => {
expect(lerpBigInt(0n, 10n, 0.29)).toBe(2n); // 2.9 -> 2
expect(lerpBigInt(0n, -10n, 0.29)).toBe(-2n); // -2.9 -> -2 (toward zero, asymmetric)
});
it('stays exact for very large bigint ranges', () => {
expect(lerpBigInt(0n, 10n ** 30n, 0.5)).toBe(5n * 10n ** 29n);
});
it('interpolates a reversed (start > end) range', () => {
expect(lerpBigInt(10n, 0n, 0.5)).toBe(5n);
expect(lerpBigInt(10n, 0n, 0.25)).toBe(8n); // 10 + (-10 * 0.25) = 7.5 -> 8? truncation
});
});
describe('inverseLerpBigInt', () => {
@@ -61,23 +75,20 @@ describe('inverseLerpBigInt', () => {
expect(result).toBe(0);
});
it('handles the maximum safe integer correctly', () => {
const result = inverseLerpBigInt(0n, MAX_SAFE_INTEGER, MAX_SAFE_INTEGER);
expect(result).toBe(1);
it('returns 1 at the maximum safe integer', () => {
expect(inverseLerpBigInt(0n, MAX_SAFE_INTEGER, MAX_SAFE_INTEGER)).toBe(1);
});
it('handles values just above the maximum safe integer correctly', () => {
const result = inverseLerpBigInt(0n, MAX_SAFE_INTEGER, 0n);
expect(result).toBe(0);
it('returns 0 at the start of a max-safe-integer range', () => {
expect(inverseLerpBigInt(0n, MAX_SAFE_INTEGER, 0n)).toBe(0);
});
it('handles values just below the maximum safe integer correctly', () => {
const result = inverseLerpBigInt(0n, MAX_SAFE_INTEGER, MAX_SAFE_INTEGER);
expect(result).toBe(1);
it('returns the midpoint of a max-safe-integer range', () => {
// 6-decimal SCALE quantizes the result, so allow ~1e-6 tolerance.
expect(inverseLerpBigInt(0n, MAX_SAFE_INTEGER, MAX_SAFE_INTEGER / 2n)).toBeCloseTo(0.5, 5);
});
it('handles values just above the maximum safe integer correctly', () => {
const result = inverseLerpBigInt(0n, 2n ** 128n, 2n ** 127n);
expect(result).toBe(0.5);
it('handles values far beyond 2^53', () => {
expect(inverseLerpBigInt(0n, 2n ** 128n, 2n ** 127n)).toBe(0.5);
});
});
@@ -1,4 +1,4 @@
import { describe, it, expect } from 'vitest';
import { describe, expect, it } from 'vitest';
import { maxBigInt } from '.';
describe('maxBigInt', () => {
@@ -1,4 +1,4 @@
import { describe, it, expect } from 'vitest';
import { describe, expect, it } from 'vitest';
import { minBigInt } from '.';
describe('minBigInt', () => {
@@ -29,4 +29,14 @@ describe('remapBigInt', () => {
// input range is zero (should return output min)
expect(remapBigInt(5n, 0n, 0n, 0n, 100n)).toBe(0n);
});
it('stay exact for large ranges (no number round-trip)', () => {
// 1/3 of 10^30, computed entirely in BigInt — only truncation, no float precision loss
expect(remapBigInt(1n, 0n, 3n, 0n, 10n ** 30n)).toBe(333333333333333333333333333333n);
});
it('preserve precision well beyond 2^53', () => {
const huge = 10n ** 40n;
expect(remapBigInt(1n, 0n, 2n, 0n, huge)).toBe(huge / 2n);
});
});
@@ -1,5 +1,4 @@
import { clampBigInt } from '../clampBigInt';
import { inverseLerpBigInt, lerpBigInt } from '../lerpBigInt';
/**
* @name remapBigInt
@@ -21,5 +20,7 @@ export function remapBigInt(value: bigint, in_min: bigint, in_max: bigint, out_m
const clampedValue = clampBigInt(value, in_min, in_max);
return lerpBigInt(out_min, out_max, inverseLerpBigInt(in_min, in_max, clampedValue));
// Stay entirely in BigInt — round-tripping through a JS number (as inverseLerpBigInt does)
// quantizes to ~6 decimals and overflows precision past 2^53, defeating the point of BigInt.
return out_min + ((clampedValue - in_min) * (out_max - out_min)) / (in_max - in_min);
}