AI 资讯
Guardrails for LLM Apps in Java
Introduction Every post in this series has quietly touched a piece of the same problem. Building Agentic Workflows in Java said toolUse.input() is untrusted and must be validated before it reaches your code. Building Reliable LLM Applications in Java said the model will confidently invent facts, so ground it and get typed output instead of parsing prose. Neither post named the thing underneath both statements: anything that crosses from outside your code into the model, or from the model back into your code, is untrusted input — a request body from the network, not a trusted internal value. This post names that boundary directly and gathers the defenses in one place — prompt injection (direct and indirect), input validation, output validation, and PII redaction — with the SAFE pattern shown beside every unsafe one it replaces, since this is the security-forward capstone of the series. The Trust Boundary: Three Kinds of Untrusted Input An LLM application has three places where untrusted text enters: User input — anything a person types, uploads, or submits through an API. Retrieved content — Making RAG Accurate in Java built a pipeline that ranks and returns chunks from a document store; those chunks were written by whoever authored the source document, not by you, and a malicious or compromised document can carry text aimed at the model reading it, not at a human reader. Model output — untrusted the moment it's about to be used rather than displayed : passed to a tool, interpolated into a query, or fed into another LLM call as context. A model that just read attacker-controlled retrieved text can be manipulated into producing attacker-controlled output. The single rule under all three: text is data until your code has explicitly decided it's safe to use for anything more than display. Nothing below is executed against a live API — every snippet is illustrative, and none of it uses a real key or a real record. Direct Prompt Injection: Defending the System Prompt A di
AI 资讯
Prompt Caching and Cost Control in Java
Introduction We already covered picking the right model tier for the task and caching a large shared prefix in https://pg-blogs.netlify.app/posts/11-building-reliable-llm-apps-in-java/ . Those two lines were the tip of a bigger discipline: LLM cost is not a fixed line item, it's an engineering variable — one you can measure and shrink with the same rigor you'd apply to database query time or container memory. This post goes deeper: how input/output pricing actually works, the exact cache_control shape and how to prove a cache hit rather than assume one, the Batches API for work that isn't latency-sensitive, and model routing — using a cheap model to triage, escalating only the hard cases to a stronger one. The honest framing throughout: measure before you optimize. Every technique here has a cost of its own; applied to the wrong workload, "optimization" makes things slower or more expensive. Token Economics: Why the Prefix Is the Bill Anthropic (like every hosted LLM provider) prices input and output tokens separately, and output is always pricier — the model has to generate output autoregressively, one token informed by all the ones before it, while input can be processed in parallel. Representative pricing from the current model catalog: Model Input Output Claude Opus 4.8 $5.00 / MTok $25.00 / MTok Claude Sonnet 5 $3.00 / MTok $15.00 / MTok Claude Haiku 4.5 $1.00 / MTok $5.00 / MTok Two consequences follow directly: Long system prompts, tool definitions, and RAG context are read on every request , not written once. A 20K-token system prompt sent on every one of 10,000 requests is 200M input tokens — at Opus 4.8 rates, $1,000 before a single output token is generated. The shared prefix , not the user's question, is usually where the money goes. A verbose model wastes money twice — once on the extra output tokens themselves, and again because the next turn's messages history now carries that verbosity forward as input on every subsequent call. Trimming max_tokens an
AI 资讯
Evaluating LLM Apps in Java
Introduction Building Reliable LLM Applications in Java put it plainly: treat model output as a hypothesis to verify, not a fact to trust. Testing Best Practices in Java put the same discipline in JUnit terms: a suite only earns trust by asserting the right things at the right level, unhappy paths included. This post is where those two ideas meet — a JUnit test either passes or fails against a fixed expected value; an LLM's output is a paragraph of prose that might be right in spirit while differing token-for-token from anything you wrote down in advance. Evaluating it takes a harness, not an assertEquals . That harness has three parts: a golden dataset of representative cases with known-good expected behavior, scoring that turns each case into a pass/fail or a number, and regression testing that runs the harness on every change and fails the build when the score drops. Making RAG Accurate in Java already gave you half of this story — recall@k, precision@k, MRR, nDCG measure whether retrieval found the right chunks. This post measures the other half: whether the generated answer built from those chunks is actually good, which is a genuinely different question a retrieval metric can't answer on its own. Everything below is illustrative, non-executed Java, grounded in the same Anthropic Java SDK shapes as posts 10/11. The Golden Dataset: Curating Cases, Not Just Inputs A golden dataset is a small, hand-curated set of (input, expected behavior) pairs that represents the ways your application is actually used — not a random sample, and not just the cases that already work. Each case needs enough structure to be scored automatically later: public record EvalCase ( String id , String category , // "extraction", "qa", "summarization", ... String input , // the prompt/question sent to the system under test String expectedExact , // non-null only for cases scorable by exact/programmatic match List < String > mustContain , // key facts a correct answer must mention (programma
AI 资讯
The Model Context Protocol in Java
Introduction Every agent needs tools, and every tool needs a way to reach the model. Building Agentic Workflows in Java built that connection by hand — a hand-written Tool schema, a loop that dispatches on toolUse.name() . LLM Frameworks vs. the Raw SDK in Java showed LangChain4j and Spring AI turning an annotated Java method into that same schema via reflection. Both are still bespoke : the tool lives inside one process, wired to one agent, in one language. The Model Context Protocol (MCP) solves a different problem: it standardizes the wire format between an AI application and a tool server, so the server doesn't have to be rewritten per agent, per framework, or per language. This post covers what that buys you, builds a minimal MCP server and a client that consumes it — both on the official Java SDK — and gives an honest answer to when reaching for a protocol is worth it over a direct tool call. The Problem MCP Solves Without a shared protocol, every pairing of agent framework and tool needs its own glue code: a LangChain4j tool wrapper, a Spring AI @Tool method, a hand-rolled schema for the raw SDK — three integrations for one capability, repeated for every tool and every framework you add. That's an M×N integration problem. MCP flattens it to M+N. A server exposes tools, resources, and prompts once, over a standard JSON-RPC protocol. Any host application — Claude Code, Claude Desktop, VS Code, or your own agent — creates an MCP client that speaks that same protocol, regardless of which framework built the host. Write the server once; every MCP-aware host can use it without new integration code. The protocol itself is intentionally boring: JSON-RPC 2.0 messages for lifecycle negotiation, tool discovery, and tool execution. Discovery ( tools/list ) and execution ( tools/call ) are the two calls that matter for this post: // tools/list response (abbreviated) { "jsonrpc" : "2.0" , "id" : 2 , "result" : { "tools" : [ { "name" : "get_account_balance" , "description"
AI 资讯
OrinIDE v1.0.9 — local AI, an Agentic dev squad, and a bug fix I owe you an explanation for
Hey devs 👋 OrinIDE is an AI-powered code editor that runs entirely in your browser — no...
AI 资讯
10 Cool CodePen Demos (June 2026)
Sand bottle - WebGPU Remember those bottles filled with colored sand that you can find in many souvenir stores? Liam Egan created a digital version using JavaScript WebGPU API. Click to drop sand, use the arrows to tilt and shake the bottles, and relax while enjoying this sandy demo. Button State Builder Margarita shared this button builder that allows to customize a control with icons, text, color, shape... even all the behavior in the different states of the button. Then you can easily get the HTML, CSS, and JS code to put it on any website. Pretty cool. WebGL Switch Button In this demo, the whole page turns into a giant three-dimensional toggle switch that can be activated clicking anywhere on the viewport. Explore the component: mouse over to make the component tilt or scroll to zoom in and out. A nice job by Toc. Animated radial gradient mask over text This demo is exactly what the title says: a radial gradient applied as a mask to some text. Cassidy aligned it perfectly with the hole in the O from "Hello" that makes this effect chef's kiss . You will need to uncomment the animation property in CSS to see the demo in action. 221. ycw always creates impressive and original content. And this demo delivers. It's not only the effect in itself, but the use of light and shadows, and the perfecto choice of color that adds a timeless atmosphere. Beautiful. vRLbdoSAIsoSQvisac Mustafa Enes created different versions of this idea over the past month, all of them are great, but I picked this one as it is more interactive. Click on the screen to regenerate the pattern and move the mouse around to animate the colors. I don't know why, but there's a feeling of peace and joy while doing it. beach sunset I saw several demos by Vivi Tseng that caught my attention this month. Really enjoyed the general minimalistic style they all had and finally picked this animated one because it feels simple and pure, almost like a drawing a child would do during a vacation. I really enjoyed it
AI 资讯
At Last, I clasp: Escaping the G's Apps Script Copy-Paste Gauntlet
Hello, I'm Maneshwar. I'm building git-lrc, a Micro AI code reviewer that runs on every commit. It is...
AI 资讯
Your fetch() Is Still Running After the User Left
When you fire a fetch() and the component that triggered it unmounts, the request keeps going. The server still processes it. When the response arrives, it calls back into whatever JavaScript it finds — a stale closure, a dead state setter, a global store that has already moved on. React's "Can't perform a state update on an unmounted component" warning is the polite version of this. The silent version is worse: results from an old query overwriting the current UI. These aren't mysterious race conditions. They're the predictable result of starting async work and never telling it to stop. The race condition hiding in every search box The search input is the clearest example. The user types "reac", your debounce fires a request. Before it lands, they finish typing "react" and you fire another. Two requests, in flight at the same time, and no guarantee about which one finishes first. If the "reac" request happens to be slower — network jitter, a cache miss, a heavier result set — it will land after "react" and overwrite the correct results with the wrong ones. The bug reproduces maybe one time in twenty on a local dev server, and consistently in production on a slow connection. The fix isn't smarter debouncing. It's cancelling the previous request when a new one starts. AbortController in plain terms AbortController is a browser-native API for cancelling async work. You create a controller, pass its signal to fetch() , and call controller.abort() to cancel. If the response hasn't arrived yet, the fetch promise rejects with an AbortError . const controller = new AbortController (); fetch ( ' /api/search?q=react ' , { signal : controller . signal }) . then ( res => res . json ()) . then ( data => setResults ( data )) . catch ( err => { if ( err . name === ' AbortError ' ) return ; // expected — not a real error setError ( err ); }); // Somewhere else, when we no longer need this request: controller . abort (); Two things to internalize: signal is how the controller knows
AI 资讯
Why v7 UUIDs beat v4 for database keys (and how to hand-roll both)
I build one small browser tool a day and write down what I learned. Day 25 was a UUID generator. What started as "make some random IDs" turned into a proper look at how the bits are laid out, and why the newer v7 format is quietly the better default for a primary key. Live tool: https://dev48v.infy.uk/solve/day25-uuid.html A UUID is just 16 bytes with a few fixed bits A UUID is a 128-bit number, written as 32 hex digits grouped 8-4-4-4-12 . That is about 3.4x10^38 possible values, which is the whole point: any machine can pick one and trust it will not clash with any other UUID minted anywhere, ever. It carries no meaning — it is an identifier, not data. The reason UUIDs exist at all is coordination. The classic database ID is 1, 2, 3... from a central counter, and that works great until you have more than one writer. Two servers, an offline mobile app, or a sharded database cannot all ask one counter for the next number without a round-trip and a lock. UUIDs sidestep that entirely: each node generates its own IDs locally, with zero coordination, and they still do not collide. A client can even create the ID before the row ever reaches the server. Version 4: 122 random bits v4 is the one most people mean by "UUID". Fill all 16 bytes with cryptographic randomness, then overwrite two small fields so tools can recognise the format: const b = new Uint8Array ( 16 ); crypto . getRandomValues ( b ); // never Math.random() b [ 6 ] = ( b [ 6 ] & 0x0f ) | 0x40 ; // version 4 b [ 8 ] = ( b [ 8 ] & 0x3f ) | 0x80 ; // variant 10xx Two things get pinned. The high nibble of byte 6 becomes 4 — that is the digit right after the second hyphen, and it is how any parser knows the scheme. The top two bits of byte 8 become 10 , which is why the 17th hex digit of almost every UUID you see is 8 , 9 , a or b . Everything else stays random: 122 bits of it. Is "random and never collides" a contradiction? The birthday paradox says collisions become likely around the square root of the space, w
AI 资讯
How to Build an Unblockable AI Agent for Browser Automation with Node.js, Bright Data, Gemini, and Playwright
In this full guide, you’ll learn: 📛 Why most AI browser agents fail on modern websites. 🧱 How browser fingerprinting and anti-bot systems work. ⛑️ How to build an AI browser agent using JavaScript (Node.js) that combines Gemini, Playwright , and Bright Data to browse real websites, extract live data, analyze, reason, and generate reports locally without maintaining fragile anti-bot infrastructure ourselves that breaks 5 days later. 🗃️ How to setup Bright Data production-ready browser sessions for AI agent automation without user’s assistance manually. 🪁Introduction Building unrestricted anonymous browser automation has developed far beyond writing Playwright scripts that click buttons and scrape HTML. Modern websites actively detect automated traffic using browser fingerprints , TLS signatures , IP reputation, and behavioral analysis, making reliable automation significantly more challenging than it was just a few years ago. Modern AI browser agents don’t usually fail because they’re arbitrary. Their reasoning, prompts, and planning loops are often sophisticated. The execution layer underneath is fragile. Most tutorials show how to connect an LLM to a browser, execute a few Playwright commands , and declare you’ve built an autonomous agent. await page . goto ( url ) await page . click ( selector ) await page . type ( selector , text ) In reality, you’ve ONLY automated a browser. Commercial sites don’t gauge how intelligent your agent is. They judge whether they believe your browser is genuine. Before a page even finishes loading, they inspect what your browser actually is: the TLS handshake , IP reputation, browser fingerprints, canvas and WebGL fingerprints , cookies, device characteristics, and even the rhythm of your connection. Dozens of signals are examined in the time it takes the page to start loading. If those signals don’t look authentic, your agent rarely reaches the real application. Instead, it encounters CAPTCHA challenges, verification pages, silent re
AI 资讯
Java & AI: What Developers Need to Know
Stop the ReAct Chaos: Building Deterministic Multi-Agent Cycles with Spring AI Graph If you are still letting LLMs freely decide their next execution step in an unconstrained ReAct loop, you are burning cloud budget on infinite loops and non-deterministic failures. In 2026, enterprise-grade AI requires the strict guardrails of stateful, cyclic graphs where transitions are governed by code, not LLM vibes. Why Most Developers Get This Wrong Naive ReAct Loops: Relying entirely on prompt-based tool calling to determine flow, which inevitably derails after 3-4 turns. Stateless Agents: Passing massive, unmanaged chat histories back and forth instead of maintaining a single, thread-safe state object. Lack of Edge Controls: Failing to hardcode conditional transitions, letting the LLM hallucinate its way into non-existent API endpoints. The Right Way The solution is to model your multi-agent system as a deterministic, cyclic graph where the LLM only executes node-level tasks, while Java code controls the state transitions. Define an Immutable State: Use Java record types to represent the thread-safe state passed between nodes. Explicit Nodes and Edges: Map agents (e.g., Writer, Critic) to discrete nodes and use conditional routers to decide the next transition. Spring AI Graph API: Leverage Spring AI 1.2.0's StatefulGraph to manage state persistence and concurrent transitions out-of-the-box. Model Specialization: Use fast, cheap models (like Llama 3.3) for routing decisions, and reasoning models (like Claude 3.5 Sonnet) only for complex node tasks. Show Me The Code (or Example) // Define stateful graph with immutable State record var workflow = new StatefulGraph < AgentState >() . addNode ( "writer" , state -> writerAgent . call ( state )) . addNode ( "critic" , state -> criticAgent . call ( state )) . addEdge ( START , "writer" ) . addEdge ( "writer" , "critic" ) . addConditionalEdge ( "critic" , state -> { return state . isApproved () ? END : "writer" ; // Deterministic cy
AI 资讯
Building a real-time gold & FX price ticker with WebSocket (Socket.IO)
If you build apps for jewelers, fintech dashboards, or e-commerce price automation, you eventually need one thing: reliable, low-latency gold and currency prices . Scraping fragile sources breaks constantly. A dedicated price API solves this. In this post I'll show how to consume real-time gold (gram, quarter, coin) and FX rates over both REST and WebSocket (Socket.IO) using the Hasfiyat Gold & Currency API . Why a price API instead of scraping? Stability — a documented contract instead of HTML that changes without notice. Low latency — prices are pushed as the market moves, not on a slow cron. Multiple sources with failover — if one provider drops, the feed keeps flowing. 1. Polling with REST The simplest integration: request the prices you need with your API key. curl -X GET \ 'https://api.hasfiyat.com/api/prices?symbols=HAS,GRAM,CEYREK' \ -H 'Authorization: Bearer YOUR_API_KEY' \ -H 'Accept: application/json' // Node.js const res = await fetch ( " https://api.hasfiyat.com/api/prices?symbols=HAS,GRAM,CEYREK " , { headers : { Authorization : " Bearer YOUR_API_KEY " } } ); const data = await res . json (); console . log ( data ); REST is ideal for periodic reporting, server-side jobs, and updating e-commerce product prices. 2. Live updates with Socket.IO For price screens, signage, and mobile apps where every tick matters, keep a connection open and let the server push changes: import { io } from " socket.io-client " ; const socket = io ( " https://api.hasfiyat.com " , { auth : { token : " YOUR_API_KEY " } }); socket . on ( " gold_prices " , ( data ) => { // { symbol: "HAS", type: "Has Altın", buy: 2450.85, sell: 2455.10, timestamp: "14:32:01.045" } console . log ( data ); }); No polling, no hammering the server — each market move arrives instantly. 3. A minimal live ticker in the browser <div id= "gold" ></div> <script src= "https://cdn.socket.io/4.7.5/socket.io.min.js" ></script> <script> const socket = io ( " https://api.hasfiyat.com " , { auth : { token : " YOUR
AI 资讯
Stop Creating a React Project Just to Preview a JSX File
If you're using AI coding assistants like ChatGPT, Claude, Cursor, or Lovable, you've probably accumulated dozens of JSX components. Generating them is incredibly fast. Previewing them? Not so much. The Typical Workflow Every time I received a JSX component, I found myself repeating the same process. Create a React project (or open an existing one) Copy the JSX file Install dependencies Fix missing imports Run the development server Wait for everything to compile All of that... just to see one component. It felt like unnecessary overhead. There Had to Be a Better Way I asked myself a simple question: Why can't I just double-click a JSX file and preview it? We can instantly open images, PDFs, videos, and text files. Why should JSX files require an entire development environment? That's what inspired me to build PreviewKit . What is PreviewKit ? PreviewKit is a lightweight Windows application that lets you preview frontend components instantly. Supported file types include: ✅ JSX ✅ Vue ✅ HTML No project setup. No dependency installation. No terminal commands. Just open the file and see the result. Why I Built It AI has dramatically changed frontend development. We're no longer spending most of our time writing components—we're reviewing, comparing, and refining them. That means fast visual feedback is more important than ever. I wanted a tool that removed the repetitive setup process so I could focus on building better interfaces instead of preparing a preview environment. Who Is It For? PreviewKit is useful if you: Build React applications Work with Vue components Test standalone HTML files Generate UI with AI tools Review components from teammates Prototype interfaces quickly If opening frontend files feels slower than it should, PreviewKit was built for you. The Goal Isn't to Replace Your Framework You'll still use React. You'll still use Vue. You'll still use Vite or Next.js. PreviewKit isn't trying to replace your existing workflow. It simply removes one frustrat
开发者
"Four Remote Job Boards Have Free Public APIs. Here Is One Schema for All of Them"
If you want remote job data, you do not need to scrape HTML or sign up for anything. Four of the bigger remote job boards publish keyless public feeds. The catch is that they all speak different dialects, so the real work is normalization. Here are the endpoints and the traps. The four feeds RemoteOK returns its whole current board as one JSON array: GET https://remoteok.com/api The first element is a legal notice, not a job: they ask for a link back with attribution as a condition of using the feed. Skip element zero, and honor the attribution if you republish. Jobs carry salary_min and salary_max as numbers, tags, and ISO dates. Remotive has the friendliest API of the four, including server side search: GET https://remotive.com/api/remote-jobs?search=python&limit=100 Salary here is free text ( "$120k - $160k" ), so do not expect numbers. Attribution with a link back is required here too. WeWorkRemotely publishes RSS: GET https://weworkremotely.com/remote-jobs.rss Two quirks: the company name is not a field, it is baked into the title as Company: Role , so split on the first colon. And useful data hides in nonstandard tags like <region> , <skills> , and <category> that generic RSS parsers drop on the floor. Himalayas has a proper paginated API with a surprisingly deep catalog (100k+ listings): GET https://himalayas.app/jobs/api?limit=100&offset=0 It gives structured minSalary / maxSalary with a currency and period, seniority arrays, location restrictions, and even timezone restrictions as UTC offsets. Dates are epoch seconds, not ISO strings. The normalization layer The row schema that survived contact with all four sources: { "source" : "Remotive" , "title" : "Senior Backend Engineer" , "company" : "Acme Corp" , "tags" : [ "python" , "aws" ], "salaryMin" : null , "salaryMax" : null , "salaryText" : "$120k - $160k" , "location" : "Worldwide" , "postedAt" : "2026-07-03T20:01:13.000Z" , "applyUrl" : "https://..." } Rules that mattered in practice: Keep both salary sh
AI 资讯
Database Indexing and Query Optimization for Java Developers
Introduction Fixing N+1 queries (see the previous post ) gets your Hibernate app down to a handful of queries per request. The next bottleneck is what each of those queries costs once your tables have millions of rows — and that is almost always a question of indexing. An index turns "scan every row" into "look it up directly." Get the index wrong — or skip it — and a query that took 2ms in development takes 4 seconds in production once real data volume shows up. How Indexes Work: The B-Tree Intuition Without an index, a WHERE clause forces a sequential scan : the database reads every row and checks the condition. That's O(n) — cost grows linearly with table size. An index is a separate, sorted data structure (almost always a B-tree ) that maps column values to row locations. Because it's sorted and balanced, finding a value is a tree walk: O(log n) . On a 10-million-row table, that's the difference between reading 10 million rows and reading roughly 23 tree nodes. The cost is not free: Writes get slower. Every INSERT / UPDATE / DELETE on an indexed column must also update the index structure. Storage grows. Each index is a copy of (part of) the data, sorted differently. An index is a trade: you pay on every write so that specific reads become fast. Indexing a column you rarely filter or sort on is pure cost with no benefit. Reading Query Plans: EXPLAIN ANALYZE Postgres' EXPLAIN ANALYZE shows what the planner actually did — not what you hope it did. Before an index , filtering orders by customer_id : EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 48291 ; Seq Scan on orders (cost=0.00..21453.00 rows=42 width=96) (actual time=0.021..118.442 rows=41 loops=1) Filter: (customer_id = 48291) Rows Removed by Filter: 1199959 Planning Time: 0.112 ms Execution Time: 118.471 ms Seq Scan means Postgres read all ~1.2 million rows and threw away all but 41 of them. actual time is the real elapsed time, not an estimate — 118ms for one lookup. After CREATE INDEX idx_orders
开源项目
🔥 spicetify / cli - Command-line tool to customize Spotify client. Supports Wind
GitHub热门项目 | Command-line tool to customize Spotify client. Supports Windows, macOS, and Linux. | Stars: 23,582 | 28 stars today | 语言: JavaScript
AI 资讯
wa.me/username doesn't work yet — I verified it two ways
wa.me/username doesn't work yet — I verified it two ways, here's what to use instead If you've tried to build a "share my WhatsApp" link using a @username instead of a phone number, you've probably assumed wa.me/username (or wa.me/u/username ) works the same way wa.me/15551234567 does. It doesn't — at least not yet, as of writing this. I wanted a definitive answer instead of trusting blog posts or AI chatbot answers (more on that below), so I tested it two independent ways. Test 1: server response curl -I https://wa.me/u/some_real_reserved_username Every username path I tried — including a certified-real, currently-reserved username — 302-redirects to: api.whatsapp.com/resolve/?deeplink=...¬_found=1 Compare that to the phone-number path, which redirects to: api.whatsapp.com/send/?phone=...&type=phone_number Different resolver, different outcome. The server-side route for usernames exists, but every lookup currently resolves as "not found" — even for real, live usernames. Test 2: real device Server response alone doesn't rule out Universal Links / App Links intercepting the URL client-side before it ever hits a server — curl can't see that. So I also opened all three link variants ( wa.me/username , wa.me/u/username , and a redirect through my own domain) on a real phone with WhatsApp installed. None of them opened a chat. Why this matters if you're building anything around WhatsApp usernames WhatsApp has rolled out @username handles as a real, user-facing feature — but it hasn't published a public deep-link spec for opening a chat from one, the way it has for phone numbers for years. If you're building a tool, a profile page, a business card generator, anything that assumes wa.me/username "just works," it doesn't, for anyone. One more data point: I asked Meta AI directly about this, with the counter-evidence above in hand. It kept asserting the link already works and didn't engage with the evidence when pushed. That's a useful reminder that chatbot answers about
开发者
From 0 Likes to Meme Engineer
We have all been there. You are sitting at your desk late at night, your code is throwing errors that...
AI 资讯
How to Compress Images in the Browser with Canvas API (No Uploads, No Server)
How to Compress Images in the Browser with Canvas API Every image you upload to a "free" online compressor is sent to a server — often without you knowing what happens to it afterward. For a tool that processes your private photos, that's a terrible design. Here's how to build (or use) an image compressor that runs entirely in the browser using the HTML5 Canvas API. No uploads, no server costs, and unlimited file sizes. The Core Technique: Canvas toBlob() The key API is HTMLCanvasElement.toBlob() : js const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const img = new Image(); img.onload = () => { canvas.width = img.naturalWidth; canvas.height = img.naturalHeight; ctx.drawImage(img, 0, 0); canvas.toBlob((blob) => { const url = URL.createObjectURL(blob); }, 'image/jpeg', 0.8); }; img.src = 'your-image.jpg'; The second parameter is the MIME type (image/jpeg, image/png, image/webp, image/avif). The third is quality (0–1). Step-Down Resizing for Large Images If you're compressing a 6000×4000 px photo, drawing it at full resolution onto a canvas can eat 70+ MB of memory. Step-down resizing halves the dimensions repeatedly: function stepDownEncode(img, maxDim, quality) { let w = img.naturalWidth; let h = img.naturalHeight; let src = img; while (w > maxDim * 2 || h > maxDim * 2) { w = Math.floor(w / 2); h = Math.floor(h / 2); const temp = document.createElement('canvas'); temp.width = w; temp.height = h; temp.getContext('2d').drawImage(src, 0, 0, w, h); src = temp; } const canvas = document.createElement('canvas'); canvas.width = w; canvas.height = h; canvas.getContext('2d').drawImage(src, 0, 0, w, h); return new Promise((resolve) => { canvas.toBlob((blob) => resolve(blob), 'image/jpeg', quality); }); } This prevents memory crashes and actually produces better quality (step-down preserves more detail than a single jump). Comparing Real-World Results Format Avg Original Avg Compressed Avg Savings JPEG → JPEG (Q80) 3.2 MB 0.8 MB 75% PNG → We
AI 资讯
Why I Ditched Socket.IO for Raw WebSockets (And What I Learned)
When you google "how to build a chat app in Node.js," the very first result will almost certainly point you to Socket.IO. It is the de facto standard for a reason. When I started my project, I used it without a second thought. It worked like magic. But as I got deeper into the project, that magic started to feel more like a black box. I eventually ripped out Socket.IO and replaced it with raw, native WebSockets. It was a daunting decision, but having built and managed it myself, I have some strong opinions on what Socket.IO abstracts away, what I had to build from scratch, and whether the headache was actually worth it. The Magic of Socket.IO (And Why We Use It) To understand why walking away from Socket.IO is hard, you have to understand exactly how much heavy lifting it does for you behind the scenes. It isn't just a WebSocket library; it is a real-time framework. The Polling Fallback: Historically, if a user's corporate firewall blocked WebSockets, Socket.IO would seamlessly downgrade to HTTP long-polling. Automatic Reconnections: If a user drives through a tunnel and loses the connection, Socket.IO automatically handles the exponential backoff to reconnect them when they emerge. Rooms and Namespaces: It gives you a beautiful socket.to("room-1").emit() API for broadcasting messages to specific groups of users. Heartbeats: It manages ping/pong messages under the hood to ensure the connection hasn't silently died. When you drop Socket.IO, you lose all of this for free. So, Why Did I Walk Away? First, the fallback mechanism is largely a relic of the past. Today, native WebSocket support across modern browsers and network infrastructure is essentially ubiquitous. I didn't need to ship a massive client bundle just to support HTTP polling for the 0.1% of edge cases. Second, the lock-in is real. If you use Socket.IO on the client, you must use a Socket.IO server implementation. You can't just connect to a standard WebSocket server. I wanted the freedom to swap out my ba