惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

The Hacker News
The Hacker News
H
Hackread – Cybersecurity News, Data Breaches, AI and More
小众软件
小众软件
云风的 BLOG
云风的 BLOG
Martin Fowler
Martin Fowler
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
B
Blog RSS Feed
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
博客园 - 聂微东
L
LangChain Blog
博客园 - 司徒正美
腾讯CDC
C
Cybersecurity and Infrastructure Security Agency CISA
C
Cisco Blogs
M
MIT News - Artificial intelligence
Y
Y Combinator Blog
S
Schneier on Security
T
Tailwind CSS Blog
S
Securelist
P
Proofpoint News Feed
A
Arctic Wolf
有赞技术团队
有赞技术团队
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
P
Privacy & Cybersecurity Law Blog
爱范儿
爱范儿
G
GRAHAM CLULEY
F
Full Disclosure
T
Threat Research - Cisco Blogs
Hugging Face - Blog
Hugging Face - Blog
T
Tor Project blog
T
Threatpost
月光博客
月光博客
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
C
CXSECURITY Database RSS Feed - CXSecurity.com
AWS News Blog
AWS News Blog
C
CERT Recently Published Vulnerability Notes
Apple Machine Learning Research
Apple Machine Learning Research
博客园_首页
Simon Willison's Weblog
Simon Willison's Weblog
Microsoft Security Blog
Microsoft Security Blog
雷峰网
雷峰网
I
Intezer
GbyAI
GbyAI
T
The Exploit Database - CXSecurity.com
L
LINUX DO - 热门话题
J
Java Code Geeks
I
InfoQ
Stack Overflow Blog
Stack Overflow Blog
V
Visual Studio Blog
罗磊的独立博客

seg6

concurrent device registration without redis — seg6 building a software protection system from first principles — seg6 cross-origin iframes without third-party cookies — seg6 gave my rgb fans a job: 38-pixel screen mirror — seg6 making macos bearable — seg6 hello, world! — seg6
hijacking chrome's network tab to debug an electron app — seg6
2025-12-26 · via seg6

A while back I was working on a local-first productivity app. Think notes, files, AI chat, all running on your machine with no cloud required. The frontend was Svelte running in Electron, but the core of the app lived in a Rust backend compiled as a native Node module using Neon FFI. Storage, search, embeddings, AI inference. All Rust.

The architecture was straightforward: frontend calls JavaScript functions like js__store_create_resource or js__ai_send_chat_message, those calls cross the FFI boundary into Rust, Rust does its thing with SQLite and ML models and whatever else, and returns the result. Clean separation. Rust handles the heavy lifting, JavaScript handles the UI.

This was a fast-moving project. Early stage, small team, lots of experimentation. Features got added, APIs changed, entire subsystems got rewritten. The kind of environment where you’re making decisions on the fly and figuring out the “right” way to do things later. Proper observability? Structured logging? Sure, that was on the roadmap. Somewhere there.

Then the app started hanging.

flying blind

Not crashing. Just… freezing. The UI would lock up for 10, 20, sometimes 30+ seconds. No error messages, no crash reports. Just a stuck app and a user staring at a frozen screen wondering if they should force quit.

And I had no idea what was causing it.

Was it a database query? A file operation? Something in the AI pipeline? The Rust code had grown complex. Worker threads, async channels, SQLite queries, embedding models. Any of it could be the culprit.

Here’s the thing about native FFI calls: they’re invisible. When JavaScript calls a Rust function, from the browser’s perspective, nothing happens. There’s no network request. There’s no entry in DevTools. The call just… disappears into native land and comes back whenever it feels like it.

I couldn’t see what functions were being called. I couldn’t see what arguments were passed. I couldn’t see how long each call took. The entire Rust backend was a black box.

I could scatter console.log statements everywhere. I could add timestamps before and after every call. I could instrument the Rust code with tracing spans. But this was a fire that needed to be out now, not after spending a week setting up proper observability infrastructure.

So I wrote a hack.

Chrome DevTools has a beautiful Network tab. It shows every HTTP request with timing, headers, payload, response. What if I could make my native calls show up there?

The plan:

  1. Spin up a local HTTP server in the Electron preload process
  2. Wrap every native function with a proxy that makes an HTTP POST instead
  3. The server receives the request, calls the actual native function, returns the result
  4. Now every FFI call shows up in the Network tab

For streaming callbacks (like AI chat responses that come in chunks), I’d use Server-Sent Events to pipe the data back.

building it

The app already had a clean initialization pattern. All native functions went through an initSFFS function that loaded the native module and wrapped the functions with a handle:

const sffs = require('@deta/backend')

let handle = sffs.js__backend_tunnel_init(
  rootPath, appPath, localAiMode, languageSetting,
  numWorkerThreads, numProcessorThreads,
  eventBusCallback
)

const with_handle = (fn) => (...args) => fn(handle, ...args)

// Normal mode: direct FFI calls
return {
  js__store_search_resources: with_handle(sffs.js__store_search_resources),
  js__ai_send_chat_message: with_handle(sffs.js__ai_send_chat_message),
  // ... etc
}

I added a flag: --enable-debug-proxy. When set, instead of returning direct wrappers, I’d return HTTP proxy functions.

debug server

const setupDebugServer = () => {
  server = http.createServer((req, res) => {
    res.setHeader('Access-Control-Allow-Origin', '*')
    res.setHeader('Access-Control-Allow-Methods', 'OPTIONS, POST, GET')
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type')

    if (req.method === 'OPTIONS') {
      res.writeHead(204)
      res.end()
      return
    }

    const [_, fn, action, callId] = req.url.split('/')

    if (req.method === 'GET' && action === 'stream') {
      handleSSE(res, callId)
    } else if (req.method === 'POST') {
      handlePostRequest(req, res, fn)
    } else {
      res.writeHead(404)
      res.end()
    }
  })

  server.listen(0, 'localhost', () => {
    console.log(`Debug server running on port ${server.address().port}`)
  })
}

Nothing fancy. A basic HTTP server that routes POST requests to function calls and GET requests to SSE streams.

proxy functions

Instead of calling the native function directly, the proxy makes an HTTP request:

const createProxyFunction = (key) => {
  return async (...args) => {
    const isChat = key === 'js__ai_send_chat_message'
    const callId = isChat ? Math.random().toString(36).slice(2, 11) : undefined

    if (isChat) {
      setupSSE(key, callId, args[2]) // args[2] is the callback
    }

    const response = await fetch(`http://localhost:${server.address().port}/${key}`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ args, callId })
    })

    if (!response.ok) {
      throw new Error(`HTTP error status: ${response.status}`)
    }

    return response.json()
  }
}

The server receives this, calls the real native function, and returns the result:

const handlePostRequest = (req, res, fn) => {
  let body = ''
  req.on('data', (chunk) => {
    body += chunk.toString()
  })
  req.on('end', async () => {
    const { args, callId } = JSON.parse(body)
    try {
      if (fn === 'js__ai_send_chat_message') {
        args[2] = createProxyCallback(callId)
      }
      const result = await sffs[fn](handle, ...args)
      res.writeHead(200, { 'Content-Type': 'application/json' })
      res.end(JSON.stringify(result))
    } catch (error) {
      res.writeHead(500, { 'Content-Type': 'application/json' })
      res.end(JSON.stringify({ error: error.message }))
    }
  })
}

streaming callbacks via sse

Some functions take callbacks. AI chat, for example, streams responses chunk by chunk. I couldn’t just serialize a callback function over HTTP.

The solution: Server-Sent Events. Before making the POST request, the client opens an SSE connection. The server replaces the original callback with one that emits to the SSE stream:

const handleSSE = (res, callId) => {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    Connection: 'keep-alive'
  })

  const emitter = new EventEmitter()
  callbackEmitters.set(callId, emitter)

  emitter.on('data', (data) => {
    res.write(`data: ${JSON.stringify(data)}\n\n`)
  })

  res.on('close', () => {
    callbackEmitters.delete(callId)
  })
}

const createProxyCallback = (callId) => {
  return (data) => {
    const emitter = callbackEmitters.get(callId)
    if (emitter) emitter.emit('data', data)
  }
}

On the client side, an EventSource listens for these events and calls the original callback:

const setupSSE = (key, callId, originalCallback) => {
  const eventSource = new EventSource(
    `http://localhost:${server.address().port}/${key}/stream/${callId}`
  )

  eventSource.onmessage = (event) => {
    const data = JSON.parse(event.data)
    originalCallback(data)
  }

  eventSource.onerror = () => {
    eventSource.close()
  }
}

putting it together

The initialization now branches based on the debug flag:

return {
  ...Object.fromEntries(
    Object.entries(sffs)
      .filter(([key, value]) =>
        typeof value === 'function' &&
        key.startsWith('js__') &&
        key !== 'js__backend_tunnel_init'
      )
      .map(([key, value]) => [
        key,
        ENABLE_DEBUG_PROXY ? createProxyFunction(key) : with_handle(value)
      ])
  ),
  js__backend_event_bus_register
}

Same API surface. When debug proxy is off, direct FFI calls. When it’s on, everything goes through HTTP.

what i found

With --enable-debug-proxy, I opened DevTools and watched my Network tab light up:

  • POST /js__store_search_resources - 847ms
  • POST /js__store_get_resource - 12ms
  • POST /js__store_search_resources - 23,847ms ← there’s the problem

I could see exactly which function was hanging, what arguments were passed, and how long each call took.

The 23-second search had a malformed query that was causing a full table scan. Found it in minutes, fixed it, moved on.

Network tab showing FFI calls

Inspecting the payload


Sometimes the right debugging tool is the one you build yourself in a moment of frustration. The hack stuck around and became a real feature and the rest of the team started using it too :)