r/javascript 18h ago

GitHub - unadlib/localspace: A library that unifies the APIs of IndexedDB, localStorage and other storage into a consistent API

Thumbnail github.com
3 Upvotes

r/javascript 20h ago

AskJS [AskJS] Best way to handle rotating proxies in Node.js?

1 Upvotes

I've got a Node.js script making a decent number of requests and I'm trying to decide where proxy rotation should live. One option is picking a new proxy inside the app, the other is using one endpoint and letting the proxy service rotate behind it. The second sounds cleaner, but I'm not sure how well that works when you need sticky sessions. How are you structuring this in your own projects?


r/javascript 16h ago

Headless phone input controller with no baked-in UI to fight

Thumbnail github.com
5 Upvotes

Have you ever been on a project with a polished UI library, animated inputs, a specific look, and then had to add a phone number field? A phone input is not just validation. Under the hood it is real work: formatting the number as you type, keeping the caret in place, and handling events like paste and undo. The ready-made widgets handle that, but each ships its own markup and styling. Matching one to a specific, opinionated design system does not always work.

That is exactly why I built a headless input controller. You pass your styled input into a function, whatever it looks like, and it handles the rest: formatting, validation, the input events, and the caret.

```js import { createPhoneInput } from '@telixon/web-sdk';

const phone = createPhoneInput({ mode: 'international', defaultRegion: 'US', display: { callingCodeInInput: true, plusPrefix: 'erasable' }, input: document.querySelector('input'), });

phone.subscribe((state) => { // state.value, state.region, state.validationError (a typed reason) }); ```

That one call covers the actions people actually do: paste (formatted numbers too, and it dedupes a repeated country code), drag/drop, cut, word/line delete, IME, autofill, and undo/redo. On every keystroke the state carries the fully parsed, validated number, with a typed reason when it is not valid: too short by N, a calling code that cannot exist, and so on.

Under it is a headless controller you can drive directly, with no DOM: insert, deleteBackward, deleteForward, setValue, undo, redo, region/type filters, getPhoneNumber. One controller does national, international with the calling code in or out of the field, and your own country picker. Formatting and validation come from Google's libphonenumber metadata.

Playground to try it live: telixon.dev/playground/input


r/javascript 21h ago

Zustand is now the top state management library in new JS repos, and it's still accelerating (+7.3% this week). Pulled this from a dataset of 5,000+ live repos.

Thumbnail github.com
0 Upvotes

r/javascript 14h ago

AskJS [AskJS] BeeLadybug — Open Source Canvas 2D Debug Overlay in Vanilla JS (Zero Dependencies)

0 Upvotes

I built BeeLadybug, a zero-dependency visual debugging overlay for my Vanilla JS Canvas engine. It renders AABB hitboxes in world space (green for clear, red for collisions) and a screen-space HUD with FPS, active entities, cycle time, and freeze-frame / slow-motion controls (F2/F3/F4). Here is the clean source code:

export const BEE_LADYBUG_DEFAULTS = Object.freeze({
    toggleKey: 'F2',
    slowKey: 'F3',
    freezeKey: 'F4',
    slowScale: 0.25,
    colorActive: '#3dff6a',
    colorColliding: '#ff3b3b',
    colorInactive: '#8a8a8a',
    overlayX: 12,
    overlayY: 12
});


export class BeeLadybug {
   
    constructor(engine, options = {}) {
        this.engine = engine;
        this.configure(options);


        this.enabled = false;
        this.#entities = [];
        this.#colliding = new Set();
        this.#buttons = [];
        this.#onKeyDown = (event) => this.#handleKey(event);
        this.#bound = false;
    }


    #entities;
    #colliding;
    #buttons;
    #onKeyDown;
    #bound;


    configure(options = {}) {
        const cfg = { ...BEE_LADYBUG_DEFAULTS, ...options };
        this.toggleKey = cfg.toggleKey;
        this.slowKey = cfg.slowKey;
        this.freezeKey = cfg.freezeKey;
        this.slowScale = cfg.slowScale;
        this.colorActive = cfg.colorActive;
        this.colorColliding = cfg.colorColliding;
        this.colorInactive = cfg.colorInactive;
        this.overlayX = cfg.overlayX;
        this.overlayY = cfg.overlayY;
        return this;
    }


    attach() {
        if (this.#bound) return this;
        window.addEventListener('keydown', this.#onKeyDown);
        this.#bound = true;
        return this;
    }


    detach() {
        if (!this.#bound) return this;
        window.removeEventListener('keydown', this.#onKeyDown);
        this.#bound = false;
        return this;
    }


    destroy() {
        this.detach();
        this.enabled = false;
        this.#entities.length = 0;
        this.#colliding.clear();
        this.#buttons.length = 0;
    }


    show() {
        this.enabled = true;
        return this;
    }


    hide() {
        this.enabled = false;
        return this;
    }


    toggle() {
        this.enabled = !this.enabled;
        return this;
    }


    applySlowMo() {
        const time = this.engine.time;
        if (!time) return this;
        if (time.paused) time.resume();
        const next = time.timeScale === this.slowScale ? 1 : this.slowScale;
        this.engine.setTimeScale(next);
        return this;
    }


    toggleFreeze() {
        const time = this.engine.time;
        if (!time) return this;
        time.togglePause();
        return this;
    }


    restoreRealtime() {
        const time = this.engine.time;
        if (!time) return this;
        if (time.paused) time.resume();
        this.engine.setTimeScale(1);
        return this;
    }


    #handleKey(event) {
        if (event.repeat) return;


        if (event.code === this.toggleKey) {
            event.preventDefault();
            this.toggle();
            return;
        }


        if (!this.enabled) return;


        if (event.code === this.slowKey) {
            event.preventDefault();
            this.applySlowMo();
            return;
        }


        if (event.code === this.freezeKey) {
            event.preventDefault();
            this.toggleFreeze();
        }
    }


    #collectEntities() {
        const list = this.#entities;
        list.length = 0;
        const seen = new Set();


        const visit = (entity) => {
            if (!entity || entity.destroyed || seen.has(entity)) return;
            seen.add(entity);
            list.push(entity);
            const kids = entity.children;
            if (!kids) return;
            for (let i = 0; i < kids.length; i++) {
                visit(kids[i]);
            }
        };


        const engine = this.engine;
        const roots = engine.entities;
        if (roots) {
            for (let i = 0; i < roots.length; i++) visit(roots[i]);
        }


        const scene = engine.scenes && engine.scenes.currentScene
            ? engine.scenes.currentScene
            : engine.currentScene;
        if (scene && scene.entities) {
            for (let i = 0; i < scene.entities.length; i++) visit(scene.entities[i]);
        }


        const groups = engine.collisions && engine.collisions.groups;
        if (groups && typeof groups.values === 'function') {
            for (const bucket of groups.values()) {
                if (!bucket) continue;
                for (let i = 0; i < bucket.length; i++) visit(bucket[i]);
            }
        }


        return list;
    }


    #aabbOf(entity) {
        const engine = this.engine;
        if (engine && typeof engine.getEntityDrawBounds === 'function') {
            return engine.getEntityDrawBounds(entity);
        }
        if (!entity) return null;
        return {
            x: typeof entity.worldX === 'number' ? entity.worldX : entity.x,
            y: typeof entity.worldY === 'number' ? entity.worldY : entity.y,
            width: entity.width ?? 0,
            height: entity.height ?? 0
        };
    }


    #refreshCollisions(list) {
        const colliding = this.#colliding;
        colliding.clear();


        const boxes = [];
        for (let i = 0; i < list.length; i++) {
            const bounds = this.#aabbOf(list[i]);
            if (!bounds || bounds.width <= 0 || bounds.height <= 0) {
                boxes.push(null);
                continue;
            }
            boxes.push(bounds);
        }


        for (let i = 0; i < list.length; i++) {
            const a = boxes[i];
            if (!a) continue;
            for (let j = i + 1; j < list.length; j++) {
                const b = boxes[j];
                if (!b) continue;
                if (
                    a.x < b.x + b.width &&
                    a.x + a.width > b.x &&
                    a.y < b.y + b.height &&
                    a.y + a.height > b.y
                ) {
                    colliding.add(list[i]);
                    colliding.add(list[j]);
                }
            }
        }
    }


    
    drawWorld(ctx) {
        if (!this.enabled) return;


        const list = this.#collectEntities();
        this.#refreshCollisions(list);


        ctx.save();
        ctx.lineWidth = 1.5;


        for (let i = 0; i < list.length; i++) {
            const entity = list[i];
            const box = this.#aabbOf(entity);
            if (!box || box.width <= 0 || box.height <= 0) continue;


            const colliding = this.#colliding.has(entity);
            const active = entity.active !== false;
            const color = colliding
                ? this.colorColliding
                : (active ? this.colorActive : this.colorInactive);


            ctx.strokeStyle = color;
            ctx.fillStyle = colliding ? 'rgba(255, 59, 59, 0.16)' : 'rgba(61, 255, 106, 0.08)';
            ctx.fillRect(box.x, box.y, box.width, box.height);
            ctx.strokeRect(box.x + 0.5, box.y + 0.5, box.width, box.height);
        }


        ctx.restore();
    }


    
    drawOverlay(ctx) {
        if (!this.enabled) return;


        const engine = this.engine;
        const time = engine.time;
        const list = this.#entities;
        let activeCount = 0;
        for (let i = 0; i < list.length; i++) {
            if (list[i].active !== false) activeCount += 1;
        }


        const fps = time ? time.fps : 0;
        const cycleMs = time ? time.unscaledDt * 1000 : 0;
        const scale = time ? time.timeScale : 1;
        const frozen = time ? time.paused : false;


        const x = this.overlayX;
        const y = this.overlayY;
        const width = 268;
        const height = 168;


        ctx.save();
        ctx.setTransform(1, 0, 0, 1, 0, 0);


        ctx.fillStyle = 'rgba(10, 12, 18, 0.82)';
        ctx.strokeStyle = '#ff4d4d';
        ctx.lineWidth = 2;
        if (typeof ctx.roundRect === 'function') {
            ctx.beginPath();
            ctx.roundRect(x, y, width, height, 8);
            ctx.fill();
            ctx.stroke();
        } else {
            ctx.fillRect(x, y, width, height);
            ctx.strokeRect(x, y, width, height);
        }


        ctx.fillStyle = '#ff4d4d';
        ctx.beginPath();
        ctx.arc(x + 22, y + 22, 9, 0, Math.PI * 2);
        ctx.fill();
        ctx.fillStyle = '#1a1a1a';
        ctx.beginPath();
        ctx.arc(x + 19, y + 20, 2, 0, Math.PI * 2);
        ctx.arc(x + 25, y + 20, 2, 0, Math.PI * 2);
        ctx.fill();


        ctx.fillStyle = '#ffe08a';
        ctx.font = 'bold 14px monospace';
        ctx.textAlign = 'left';
        ctx.textBaseline = 'top';
        ctx.fillText('BeeLadybug', x + 38, y + 14);


        ctx.fillStyle = '#d8d8d8';
        ctx.font = '12px monospace';
        const lines = [
            `FPS      ${fps.toFixed(0)}`,
            `ENTITA   ${activeCount} attive / ${list.length} in memoria`,
            `CICLO    ${cycleMs.toFixed(2)} ms`,
            `SCALE    ${scale.toFixed(2)}x   SIM ${frozen ? 'FREEZE' : 'RUN'}`
        ];
        for (let i = 0; i < lines.length; i++) {
            ctx.fillText(lines[i], x + 14, y + 42 + i * 16);
        }


        this.#buttons = [
            { id: 'slow', label: 'F3 SLOW', x: x + 12, y: y + 114, w: 78, h: 22 },
            { id: 'freeze', label: 'F4 STOP', x: x + 96, y: y + 114, w: 78, h: 22 },
            { id: 'live', label: '1x LIVE', x: x + 180, y: y + 114, w: 74, h: 22 }
        ];


        for (let i = 0; i < this.#buttons.length; i++) {
            const btn = this.#buttons[i];
            const hot = (btn.id === 'slow' && scale !== 1 && !frozen)
                || (btn.id === 'freeze' && frozen)
                || (btn.id === 'live' && scale === 1 && !frozen);
            ctx.fillStyle = hot ? '#ff4d4d' : '#2a2f3a';
            ctx.fillRect(btn.x, btn.y, btn.w, btn.h);
            ctx.strokeStyle = '#ffe08a';
            ctx.strokeRect(btn.x + 0.5, btn.y + 0.5, btn.w, btn.h);
            ctx.fillStyle = '#fff6d8';
            ctx.font = 'bold 10px monospace';
            ctx.textAlign = 'center';
            ctx.textBaseline = 'middle';
            ctx.fillText(btn.label, btn.x + btn.w / 2, btn.y + btn.h / 2);
        }


        ctx.textAlign = 'left';
        ctx.textBaseline = 'top';
        ctx.fillStyle = '#9aa0aa';
        ctx.font = '10px monospace';
        ctx.fillText('F2 mostra/nasconde   verde=ok  rosso=collide', x + 12, y + 146);


        ctx.restore();
    }


   
    poll() {
        if (!this.enabled) return;


        const input = this.engine.input;
        if (!input || !input.mouse || !input.mouse.wasPressed) return;


        const mx = input.mouse.x;
        const my = input.mouse.y;
        for (let i = 0; i < this.#buttons.length; i++) {
            const btn = this.#buttons[i];
            if (mx < btn.x || mx > btn.x + btn.w || my < btn.y || my > btn.y + btn.h) continue;
            if (btn.id === 'slow') this.applySlowMo();
            else if (btn.id === 'freeze') this.toggleFreeze();
            else if (btn.id === 'live') this.restoreRealtime();
            break;
        }
    }
}

r/javascript 12h ago

Can AI agents run purely in the browser with vanilla JS? Exploring $0 hosting & zero data egress

Thumbnail github.com
0 Upvotes

AI agents assume a server-side Python chain framework runtime and containers. Running client-side with JavaScript allows pushing agents entirely inside the browser, meaning $0 hosting infrastructure and zero data leaving the client.

Running client-side, you can use agents directly with WebLLM (for fully in-browser) or point them at a local Ollama / vLLM instance for 100% offline usage.

Breakdown In-Browser Loop

An in-browser agent relies on a continuous asynchronous loop that manages state, updates the DOM or application state, and invokes tools based on model outputs. Here is a minimal, clean implementation of a client-side agent loop using WebLLM's engine.chat.completions API:

```javascript import { CreateMLCEngine } from "@mlc-ai/web-llm";

async function agentLoop(task, context = {}) { const engine = await CreateMLCEngine("gemma-4-instruct"); let messages = [{ role: 'user', content: task }]; let running = true;

while (running) { const response = await engine.chat.completions.create({ messages, tools: availableTools }); const choice = response.choices[0].message;

if (choice.tool_calls) {
  for (const call of choice.tool_calls) {
    const toolResult = await executeTool(call.function.name, call.function.arguments);
    messages.push({ role: 'tool', tool_call_id: call.id, content: toolResult });
  }
} else {
  running = false;
  return choice.content;
}

} } ```


r/javascript 21h ago

I made a WebGL 2D renderer for browser games that keeps pace with PixiJS in my benchmarks

Thumbnail github.com
2 Upvotes

hi! I recently developed Rapid.js, an open-source, zero-dependency WebGL 2D renderer built specifically for browser games.

It provides an immediate-mode API and does not retain game objects or a scene graph between frames. This leaves developers free to structure their games using OOP, ECS, or any custom architecture—Rapid.js is responsible only for rendering the drawing commands submitted each frame.

It supports sprites, particles, masks, shaders, render textures, and multi-texture batching, and its UMD build is only approximately 20.6 kB gzipped.

Its small size does not come at the expense of performance: in the included browser benchmark, Rapid.js performs comparably to PixiJS.

benchmark:

https://github.com/Nightre/Rapid.js/raw/main/docs/benchmark/benchmark.png

benchmark live:

https://nightre.github.io/Rapid.js/benchmark/