npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@notrealstudio/backend-http

v0.9.1

Published

Транспортный шов nr-ui-protocol: serveBackend (node:http + SSE) и createHttpBackend (fetch)

Readme

@notrealstudio/backend-http

Транспортный шов nr-ui-protocol: обе стороны провода.

ProtocolBackend ── serveBackend(b) ── HTTP+SSE ── createHttpBackend(url) ── UI
   (объект)          (сервер §9)                    (тот же интерфейс)

Бэкенд — объект, не процесс. Где он хостится (лаунчер, service.ts, постоянный сервис) — дело потребителя; транспорт между UI и бэкендом всегда HTTP+SSE, даже in-process. UI держит ProtocolBackend и не различает, локальный он или удалённый.

Спека: nr-ui-protocol.dev/specs/backends-spec.md §3. Канон биндинга: nr-engine/docs/specs/nr-ui-protocol-spec.md §9.

Обе стороны живут в одном пакете намеренно: они обязаны эволюционировать синхронно и тестируются друг об друга (loopback).

Установка

npm i @notrealstudio/backend-http

Зависимости: node builtins + @notrealstudio/nr-ui-protocol. Ничего про pi / engine / opencode здесь нет — это провод, а не бэкенд.

serveBackend

import { serveBackend } from '@notrealstudio/backend-http'

const srv = await serveBackend(myBackend, { port: 7331 })
console.log(srv.url)   // http://127.0.0.1:7331
await srv.close()      // backend.close() не трогает — жизнь бэкенда чужая

| opt | default | что | |---|---|---| | port | 0 (свободный) | реальный порт — в srv.port | | host | '127.0.0.1' | наружу сервер не торчит без спроса | | cors | false | CORS-заголовки + preflight (dev) | | basePath | '' | '/api' → /api/sessions; srv.url уже с ним | | maxBodyBytes | 10 МБ | больше → 413 |

Поведение:

  • Ошибки → Envelope §4: NotSupportedError → {ok:false, error, code:'not_supported'} (501), прочее → {ok:false, error} (500).
  • Авария в стриме после старта → кадр run_end{status:'error'} + закрытие (заголовки уже ушли, Envelope-ошибку не отдать). До первого кадра → Envelope.
  • Обрыв SSE-коннекта ран НЕ отменяет (§5.4): итератор дочитывается до конца, ран живёт на сервере, клиент ресинкается через history. Отмена — только эндпоинтом cancel.
  • Один активный run на сессию (§10.1): второй POST /run при живом → 409 {code:'busy'}.

createHttpBackend

import { createHttpBackend } from '@notrealstudio/backend-http'

const backend = createHttpBackend('http://127.0.0.1:7331')

for await (const ev of backend.run(sid, { userMessage: 'привет' })) {
  if (ev.type === 'ask') await backend.respond!(sid, ev.askId, { allow: true })
}

| opt | default | что | |---|---|---| | fetch | globalThis.fetch | своя реализация (тесты, авторизующая обёртка) | | headers | — | заголовки транспорта; auth — вне протокола (§10.6) | | capabilitiesTtl | ∞ | мс; по умолчанию кэш живёт до close() |

Поведение:

  • Браузер и node: поверх fetch, без EventSource — ран стартует POST'ом с телом.
  • capabilities() кэшируется.
  • Обрыв стрима → синтетический run_end{status:'error', error:{code:'connection_lost'}, version:''}, а не исключение: ран мог доехать на сервере, UI различает по коду и ресинкается через history. Пустой version = «токен неизвестен».
  • Неизвестные события едут вверх opaque, не фильтруются (§1.2).
  • close() рвёт живые запросы и запрещает новые.

Опциональные методы у клиента есть всегда

Правило протокола «метод есть ⇔ capability заявлена» держит бэкенд за проводом: capabilities() асинхронны, а фабрика синхронна — узнать их к моменту сборки объекта нельзя. Вызов незаявленной операции приезжает NotSupportedError с кодом. UI и так гасит фичи по capabilities(), а не по наличию метода.

Роуты (§9)

GET    /capabilities
GET    /sessions                          POST   /sessions  { title?, model?, profile?: id | ProfileDoc }
GET    /sessions/:id                      DELETE /sessions/:id
PATCH  /sessions/:id                      { title } → rename, { meta } → meta.patch
POST   /sessions/:id/fork                 { atMessageId? }
GET    /sessions/:id/history              { messages, version }
POST   /sessions/:id/run                  → SSE-стрим RunEvent
POST   /sessions/:id/cancel               { runId }
POST   /sessions/:id/respond              { askId, answer }
POST   /sessions/:id/messages             { parts, role?: user | assistant, swipeOf? } → messages.append
PATCH  /sessions/:id/messages/:mid        { parts | text, ifHash }
DELETE /sessions/:id/messages/:mid
POST   /sessions/:id/messages/:mid/swipe  { dir | index }
POST   /sessions/:id/model                { model, thinking? } → применённое
GET    /models                            каталог ModelInfo[]
GET    /workspace                         текущее пространство WorkspaceInfo
GET    /workspaces                        известные пространства WorkspaceInfo[]
POST   /workspace                         { id } → переключить (workspace.switch)
GET    /workspace/files?path=             FileEntry[] одного уровня (без path — корень)       (workspace-files-spec §1)
GET    /workspace/file?path=&maxBytes=    FileContent
PUT    /workspace/file                    { path, content, encoding?, revision? } → { revision }; revision: null — создать
DELETE /workspace/file?path=              в корзину пространства
POST   /workspace/file/rename             { from, to }
GET    /workspace/events                  → SSE workspace_update { paths } (capability workspace.files.watch)
GET    /profiles                          каталог ProfileInfo[] (profiles-spec §4)
POST   /sessions/:id/profile              { id } | { doc } → профиль сессии (profiles.set; doc — встроенный, inline)
GET    /profiles/:id                      ProfileDoc (profile-editor-spec §3)
PUT    /profiles/:id                      ProfileDoc → сохранённый (profiles.save; id — из адреса)
DELETE /profiles/:id                      profiles.delete (default — bad_request)
POST   /profiles/:id/duplicate            { id } → копия (занятый id — conflict)
GET    /catalogs/{tools,skills,extensions} каталоги конструктора профилей
GET    /bots, /bots/:id, /bots/:id/assets/:file
GET    /personas, /personas/:id

Два роута §9 не даёт, а операции §4.2 требуют — добавлены по симметрии (обе стороны пакета их знают, спека — кандидат на правку):

GET    /sessions/:id/meta                 meta.get (в §9 только PATCH)
POST   /sessions/:id/messages/:mid/hide   { hidden }  messages.hide
GET    /workspace/file/stat?path=         workspace.files.stat (workspace-files-spec §1 роута не даёт)

Файлы пространства (workspace-files-spec §1–2)

  • Вход проверяется до бэкенда: нет path у read/delete, content не строка, чужой encoding, revision не строка/null, кривой maxBytes — 400. Ошибки бэкенда — своими кодами: conflict 409, not_found 404, bad_request 400.
  • GET /workspace/events — отдельный SSE-канал, не поток рана: комментарий : workspace events на подключение, кадры event: workspace_update, keepalive : ping раз в 25 с. Живёт, пока клиент держит соединение.
  • Клиент: workspace.files.watch(listener) → unsubscribe синхронно, канал — фоном. Обрыв и 5xx — переподключение (500 мс, удвоение до 30 с), после переподключения подписчик получает paths: [''] — «перечитай всё». 4xx и not_supported — без ретраев. close() гасит и watch.
  • createStoreBackend({ files }) — memory-FS для фикстур (createMemoryFiles, экспортирована): карта path → content, ключ с / в конце — пустой каталог. Семантика backend-fs: корзина .nr-trash, revision size:mtimeMs, ignored по .gitignore из той же карты; watch — эмуляция на собственных мутациях. Без опции capability workspace остаётся {}.

code в Envelope §4 тоже нет — едет дополнительным полем, как требует backends-spec §3.1. Клиент, не знающий про code, не ломается (§1.2: неизвестные поля игнорируются).

Мета сессии (session-meta-spec §2)

meta.patch в createStoreBackend применяет правило патча протокола (mergeSessionMeta: вложенный prompt, null удаляет ключ), а драйверу уезжает готовый документ — правило одно на все хранилища, иначе «мета» означала бы разное в фикстуре и в pi. Записывается через store.meta.set (замена документа целиком); драйвер без set удаление ключа не переживёт — вложенный merge при этом работает.

Фикстурный бэкенд для e2e фронта — createStoreBackend над @notrealstudio/nr-chat-store/memory: sessionMeta там есть из коробки.

Тесты

Loopback (test/loopback.test.ts): fake-backend (in-memory, скриптованные сценарии) → serveBackend → createHttpBackend. Все операции + полный ран (multi-message, tool-parts, ask/respond, cancel, ошибка, обрыв). Приёмка — события на клиенте равны событиям fake-бэкенда.

npm test              # vitest
npm run build         # tsc → dist
npx tsc --noEmit      # только типы