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
+14 -1
View File
@@ -1,4 +1,4 @@
import { describe, it, expect } from 'vitest';
import { describe, expect, it } from 'vitest';
import { flagsGenerator } from '.';
describe('flagsGenerator', () => {
@@ -23,4 +23,17 @@ describe('flagsGenerator', () => {
expect(() => generateFlag()).toThrow(new RangeError('Cannot create more than 31 flags'));
});
it('produce 31 distinct, orthogonal powers of two up to 2^30', () => {
const generateFlag = flagsGenerator();
const flags = Array.from({ length: 31 }, () => generateFlag());
expect(new Set(flags).size).toBe(31);
flags.forEach((flag, i) => {
expect(flag).toBe(2 ** i);
expect(flag & (flag - 1)).toBe(0); // exactly one bit set
});
expect(flags.at(-1)).toBe(2 ** 30);
});
});
+11 -2
View File
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { and, or, not, has, is, unset, toggle } from '.';
import { describe, expect, it } from 'vitest';
import { and, has, is, not, or, toggle, unset } from '.';
describe('flagsAnd', () => {
it('no effect on zero flags', () => {
@@ -61,6 +61,15 @@ describe('flagsHas', () => {
expect(result).toBe(false);
});
it('require ALL queried bits, not just any (partial overlap is false)', () => {
// 0b1000 is set but 0b0100 is not — partial overlap must be false
expect(has(0b1010, 0b1100)).toBe(false);
// both bits present
expect(has(0b1110, 0b1100)).toBe(true);
// querying zero bits is vacuously true
expect(has(0b1010, 0b0000)).toBe(true);
});
});
describe('flagsIs', () => {
+1 -1
View File
@@ -29,7 +29,7 @@ export function or(...flags: number[]) {
/**
* @name not
* @category Bits
* @description Function to combine multiple flags using the XOR operator
* @description Function to apply the bitwise NOT (complement) operator to a flag
*
* @param {number} flag - The flag to apply the NOT operator to
* @returns {number} The result of the NOT operator
+2
View File
@@ -1 +1,3 @@
export * from './flags';
export * from './helpers';
export * from './vector';
+69 -3
View File
@@ -1,4 +1,4 @@
import { describe, it, expect } from 'vitest';
import { describe, expect, it } from 'vitest';
import { BitVector } from '.';
describe('BitVector', () => {
@@ -54,10 +54,76 @@ describe('BitVector', () => {
expect(bitVector.previousBit(0)).toBe(-1);
});
it('throw RangeError when previousBit is called with an unreachable value', () => {
it('clamp an out-of-range start index and return the previous set bit', () => {
const bitVector = new BitVector(16);
bitVector.setBit(5);
expect(() => bitVector.previousBit(24)).toThrow(new RangeError('Unreachable value'));
expect(bitVector.previousBit(24)).toBe(5);
});
it('return -1 from previousBit on an empty out-of-range query', () => {
const bitVector = new BitVector(16);
expect(bitVector.previousBit(24)).toBe(-1);
});
it('toggle bits correctly', () => {
const bitVector = new BitVector(16);
bitVector.toggleBit(7);
expect(bitVector.getBit(7)).toBe(true);
bitVector.toggleBit(7);
expect(bitVector.getBit(7)).toBe(false);
});
it('find the next bit correctly', () => {
const bitVector = new BitVector(100);
const indices = [0, 1, 14, 15, 63, 64, 65, 66, 88, 99];
const result = [];
indices.forEach(index => bitVector.setBit(index));
for (let i = bitVector.nextBit(-1); i !== -1; i = bitVector.nextBit(i)) {
result.push(i);
}
expect(result).toEqual(indices);
});
it('return -1 when no next bit is found', () => {
const bitVector = new BitVector(16);
expect(bitVector.nextBit(0)).toBe(-1);
expect(bitVector.nextBit(15)).toBe(-1);
});
it('count the number of set bits', () => {
const bitVector = new BitVector(100);
expect(bitVector.count()).toBe(0);
[0, 5, 63, 64, 99].forEach(index => bitVector.setBit(index));
expect(bitVector.count()).toBe(5);
bitVector.clearBit(5);
expect(bitVector.count()).toBe(4);
});
it('tolerate out-of-bounds writes without crashing or corrupting in-range bits', () => {
const bitVector = new BitVector(16);
bitVector.setBit(3);
expect(() => {
bitVector.setBit(1000);
bitVector.clearBit(1000);
bitVector.toggleBit(1000);
}).not.toThrow();
// out-of-range reads are false; in-range state is intact
expect(bitVector.getBit(1000)).toBe(false);
expect(bitVector.getBit(3)).toBe(true);
expect(bitVector.count()).toBe(1);
});
});
+71
View File
@@ -2,7 +2,10 @@ export interface BitVectorLike {
getBit(index: number): boolean;
setBit(index: number): void;
clearBit(index: number): void;
toggleBit(index: number): void;
previousBit(index: number): number;
nextBit(index: number): number;
count(): number;
}
/**
@@ -30,7 +33,18 @@ export class BitVector extends Uint8Array implements BitVectorLike {
this[index >> 3]! &= ~(1 << (index & 7));
}
toggleBit(index: number): void {
this[index >> 3]! ^= 1 << (index & 7);
}
previousBit(index: number): number {
// Clamp an out-of-range start to the vector's bit length so a query past the end
// returns the last set bit (or -1) instead of falling through to the invariant throw.
const totalBits = this.length << 3;
if (index > totalBits)
index = totalBits;
while (index !== ((index >> 3) << 3)) {
--index;
@@ -58,4 +72,61 @@ export class BitVector extends Uint8Array implements BitVectorLike {
throw new RangeError('Unreachable value');
}
nextBit(index: number): number {
const totalBits = this.length << 3;
let i = index + 1;
if (i < 0)
i = 0;
// Finish scanning the remainder of the starting byte.
while (i < totalBits && (i & 7) !== 0) {
if (this.getBit(i))
return i;
++i;
}
// Skip over fully-empty bytes.
let byteIndex = i >> 3;
while (byteIndex < this.length && this[byteIndex] === 0)
++byteIndex;
if (byteIndex >= this.length)
return -1;
i = byteIndex << 3;
const end = i + 8;
while (i < end) {
if (this.getBit(i))
return i;
++i;
}
throw new RangeError('Unreachable value');
}
count(): number {
let total = 0;
const len = this.length;
// Indexed loop — the typed-array iterator protocol (for...of) is ~3.5x slower here.
for (let i = 0; i < len; i++) {
// Brian Kernighan's algorithm: iterate once per set bit.
let byte = this[i]!;
while (byte !== 0) {
byte &= byte - 1;
++total;
}
}
return total;
}
}