Skip to content

eccs0103/adaptive-extender

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

286 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Adaptive Extender

NPM Version License Bundle Size TypeScript

Extends native prototypes and provides missing standard utilities — with a focus on strict typing, safety, and code readability.

Change log

Installation

npm install adaptive-extender

Packages

Package Environment
adaptive-extender/core Browser, Node.js, Deno
adaptive-extender/node Node.js (includes core)
adaptive-extender/web Browser (includes core)
adaptive-extender/worker Web Worker (includes core)

// Import once — extensions are active everywhere
import "adaptive-extender/core";
import "adaptive-extender/web";    // Browser only
import "adaptive-extender/worker"; // Web Worker only

Native Extensions

Number

volume = volume.clamp(0, 1);

// Normalize into [0, 1] or remap between ranges
const normalized = currentValue.lerp(sourceMin, sourceMax);
const pixelX = angle.lerp(0, 360, 0, canvasWidth);

// True mathematical modulo — works correctly with negative numbers
const wrappedIndex = (-1).mod(items.length); // → items.length - 1

// Chainable fallback for invalid values
const ratio = (numerator / denominator).insteadNaN(0).insteadInfinity(1);

String

if (String.isWhitespace(userInput)) return;

const displayName = username.insteadEmpty("Anonymous");

"hello world".toTitleCase(); // → "Hello World"
"île-de-france".toLocalTitleCase("fr"); // → "Île-De-France"

Array

for (const index of Array.range(0, 10)) { ... }

for (const [user, role] of Array.zip(users, roles)) {
	console.log(`${user.name}: ${role}`);
}

// Collect an async iterable into an array
const packets = await Array.fromAsync(readableStream);

items.swap(0, items.length - 1);
items.resize(10, null);
const wasRemoved = items.remove(targetItem);

Math

const [integer, fractional] = Math.split(3.75); // → [3, 0.75]
Math.sqpw(sideLength); // sideLength²
Math.toRadians(90); // → Math.PI / 2
Math.toDegrees(Math.PI); // → 180

Math.meanArithmetic(1, 2, 3, 4); // → 2.5
Math.meanGeometric(4, 16);
Math.meanHarmonic(1, 2, 4);

Set / Map / Date

// Like classList.toggle
selected.toggle(item); // add if absent, remove if present
selected.toggle(item, true); // force add
selected.toggle(item, false); // force remove

// Add only if key is not already present
cache.add(requestKey, response); // → false if key exists

// Detect and replace invalid dates
const safeDate = new Date("invalid").insteadInvalid(new Date());

Promise

const request = fetch("/api/data");

if (await request.isSettled) { ... }
if (await request.isResolved) { ... }
if (await request.isRejected) { ... }

const responseData = await request.value; // throws if rejected
const rejectReason = await request.reason; // throws if resolved

Error / Global

// Wrap anything into an Error (string, undefined, object)
const error = Error.from(thrown);

// Assert non-null — throws ReferenceError with a message
const appRoot = ReferenceError.suppress(document.getElementById("app"));

throw new ImplementationError(); // sealed, cannot be subclassed

// Get type name without typeof / manual checks
typename(value); // → "String", "Null", "User", ...

Classes

Timespan

Value object for time intervals. Eliminates raw millisecond arithmetic.

import { Timespan } from "adaptive-extender/core";

const sessionDuration = Timespan.fromComponents(0, 30, 0); // 30 minutes
const uptime = Timespan.fromValue(process.uptime() * 1000);
const parsed = Timespan.parse("1.02:30:00.500"); // 1d 2h 30m 0.5s

console.log(uptime.toString()); // "0.00:12:45.123"

uptime.minutes = 0; // other components recalculate automatically

if (uptime.valueOf() > sessionDuration.valueOf()) {
	console.log("Session expired");
}

Color

Full color object with RGB, HSL, HEX support and alpha channel. Changing a component in one color space automatically recalculates the other.

import { Color, ColorFormats } from "adaptive-extender/core";

const orange = Color.fromRGB(255, 128, 0);
const parsed = Color.parse("#ff8800");

orange.hue = 200; // RGB recalculates automatically
orange.alpha = 0.8;

console.log(orange.toString({ format: ColorFormats.hsl, deep: true }));
// → "hsla(200, 100%, 50%, 0.8)"

Random

import { Random } from "adaptive-extender/core";

const random = Random.global;

random.boolean(0.9); // true with 90% probability
random.number(0, 1); // float [0, 1)
random.integer(1, 6); // int [1, 6]
random.item(options); // random element from array
random.subarray(items, 3); // 3 random elements
random.shuffle(array); // shuffle in place

// Weighted selection
const loot = random.case(new Map([
	["common", 70],
	["rare", 25],
	["legendary", 5],
]));

Vector

Mathematical vectors with a LINQ-style iteration API.

import { Vector, Vector2D, Vector3D } from "adaptive-extender/core";

const position = new Vector2D(3, 4);
const direction = Vector3D.fromScalar(1); // (1, 1, 1)

// LINQ-like methods over components
const normalized = Vector3D.fromVector(direction.map(component => component / Math.sqrt(3)));

Vector.isFinite(position);
const safePosition = position.insteadNaN(Vector2D.newZero);
const point = Vector2D.parse("(10, 20)"); // → Vector2D { x: 10, y: 20 }

Version

Immutable semantic version with major.minor.patch format.

import { Version } from "adaptive-extender/core";

const v = new Version(1, 2, 3);
console.log(v.toString()); // → "1.2.3"

const parsed = Version.parse("2.0.0");
const safe = Version.tryParse(userInput); // → Version | null

Controller

Abstract base class for async tasks with centralized error handling.

import { Controller } from "adaptive-extender/core";

class InitTask extends Controller<[port: number]> {
	async run(port: number): Promise<void> {
		const response = await fetch(`/api/init?port=${port}`);
		// ...
	}

	async catch(error: Error): Promise<void> {
		console.error("Init failed:", error.message);
	}

	async finally(): Promise<void> {
		console.log("Done.");
	}
}

// Creates an instance and runs it — errors are routed to catch(), finally() always runs
await InitTask.launch(8080);

EnvironmentProvider

Deserializes environment variables into a strictly typed model. JSON values are parsed automatically.

import { EnvironmentProvider, Model, Field, Optional } from "adaptive-extender/core";

class AppConfig extends Model {
	@Field(String)
	DB_HOST: string = "localhost";
	
	@Field(Number)
	DB_PORT: number = 5432;
	
	@Field(Optional(String))
	LOG_LEVEL?: string;
}

const config = EnvironmentProvider.resolve(process.env, AppConfig);
// config.DB_PORT is a number, not a string
// Throws TypeError if a required variable is missing or has the wrong type

Portable Data System

Binds classes to their schema via decorators. Importing from JSON validates types automatically and returns a real class instance.

Basic Usage

import { Model, Field, ArrayOf, Optional, Nullable } from "adaptive-extender/core";

class Tag extends Model {
	@Field(String)
	name: string = "";
}

class Article extends Model {
	@Field(String)
	title: string = "";
	
	@Field(Number)
	views: number = 0;
	
	@Field(ArrayOf(Tag))
	tags: Tag[] = [];
	
	@Field(Optional(String))
	subtitle?: string;
	
	@Field(Nullable(String))
	draft: string | null = null;
}

const article = Article.import(json, "api.article");
// article instanceof Article → true
// TypeError with exact path on failure: "api.article.tags[2].name"

const raw = Article.export(article); // → plain object, ready for JSON.stringify

Polymorphism

import { Model, Field, Descendant, DiscriminatorKey } from "adaptive-extender/core";

@DiscriminatorKey("kind")
@Descendant(Dog, "dog")
@Descendant(Cat, "cat")
abstract class Animal extends Model {
}

class Dog extends Animal {
	@Field(String)
	breed: string = "";
}

class Cat extends Animal {
	@Field(Boolean)
	indoor: boolean = true;
}

// { kind: "dog", breed: "Husky" } → Dog instance
// { kind: "cat", indoor: true } → Cat instance
const animal = Animal.import(json, "api.animal");

Adapters

Adapter Type
ArrayOf(T) T[]
SetOf(T) Set<T>T[]
RecordOf(T) Map<string, T>Record<string, T>
MapOf(K, V) Map<K, V>[K, V][]
Optional(T) T | undefined
Nullable(T) T | null
Deferred(_ => T) Circular references
EnumAs(E) TypeScript enum or const-object enum

Web

DOM Queries

Type-safe wrappers over querySelector — throw instead of returning null.

import "adaptive-extender/web";

const loginForm = document.getElement(HTMLFormElement, "#login-form");
const requiredInputs = loginForm.getElements(HTMLInputElement, "input[required]");
const container = loginForm.getClosest(HTMLDivElement, ".wrapper");

// Async variants — useful with Shadow DOM
const canvas = await document.getElementAsync(HTMLCanvasElement, "#scene");

Game Engines

Three update-loop engines for UI animation or game logic:

import { FastEngine, StaticEngine } from "adaptive-extender/web";

// Fast — requestAnimationFrame based
const engine = new FastEngine({ launch: true });
engine.limit = 60;

engine.addEventListener("trigger", () => {
	const deltaTime = engine.delta; // seconds since last frame
	// update scene...
});

// Static — fixed fps via setTimeout
const fixedEngine = new StaticEngine({ launch: true });
fixedEngine.limit = 30;

Archive (localStorage)

Type-safe localStorage with buffered access and auto-save.

import { ArchiveRepository, Model, Field } from "adaptive-extender/web";

class Settings extends Model {
	@Field(Boolean)
	darkMode: boolean = false;
	
	@Field(Number)
	volume: number = 1;
}

const repository = new ArchiveRepository("settings", Settings, new Settings());

const settings = repository.content;
settings.volume = 0.5;

// save() is async — resolves when the write completes, rejects on failure
await repository.save(); // save immediately
await repository.save(3000); // debounced — save after 3 seconds
// A pending save is cancelled (AbortError) when superseded by a new call
repository.reset(); // abort pending save and revert to initial state

Promise Utilities

await Promise.asTimeout(1000);

// AbortController is created and aborted automatically on completion
const result = await Promise.withSignal((signal, resolve, reject) => {
	buttonAccept.addEventListener("click", event => resolve(event.result), { signal });
	buttonDecline.addEventListener("click", event => reject(event.error), { signal });
});

Reflect Utilities

// Apply a function only if the value is not null / undefined
const upper = Reflect.mapNull(maybeNull, text => text.toUpperCase());
const trimmed = Reflect.mapUndefined(maybeUndefined, text => text.trim());
const parsed = Reflect.mapNullable(maybeNullable, text => Number.parseInt(text));

License

Apache-2.0