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

@gaonprime/nanumi-sse-client

v0.3.1

Published

Nanumi SSE client runtime for browser consumers.

Readme

@gaonprime/nanumi-sse-client

Nanumi 애플리케이션의 SSE 연결 lifecycle과 event 처리 계약을 제공할 client package입니다.

현재 공개 surface는 runtime-neutral Core, Browser Adapter, React 17+ Adapter입니다. Node Adapter는 구현 전이라 export와 tarball에서 제외합니다. Package manifest는 public npm 배포 기준으로 구성되어 있지만 실제 publish는 release acceptance를 통과한 CI에서만 수행합니다.

현재 public entrypoint는 Root, /browser, /react입니다.

설치

npm install @gaonprime/[email protected]
# 또는
yarn add @gaonprime/[email protected]

패키지는 public npm registry에서 인증 없이 설치합니다. 초기 0.x 기간에는 wire와 public API 변경을 명확히 추적하기 위해 exact version 사용을 권장합니다.

지원 범위:

  • SDK를 설치하는 소비 프로젝트: Node.js 14.21.3 이상
  • 저장소 개발·서버·CI·npm 배포 환경: Node.js 24
  • Browser: fetch, EventSource, Web Crypto, AbortController, Web Storage 지원 환경
  • React Adapter: React 17 이상
  • Node process Adapter: 아직 제공하지 않음

0.3.1부터 Browser/React SDK의 Node.js 최소 버전을 24에서 14.21.3으로 완화합니다. SDK는 빌드된 JavaScript와 타입 선언을 배포하므로 소비 프로젝트에서 SDK 소스를 빌드할 필요가 없습니다. 실제 통신은 Browser API를 사용하며, Node process에서의 SSE 실행을 지원한다는 의미는 아닙니다. 소비 앱의 빌드 도구가 요구하는 Node 버전은 별도로 적용됩니다.

Module 범위

현재 패키지가 소유하는 Implementation:

  • 하나의 SSE connection과 subscription topology
  • system/domain event 처리, cursor, replay, reconnect
  • Browser device identity, register, 기본 15분 heartbeat
  • device.* immediate lane과 Browser probe/reload/redirect 처리
  • Browser diagnostics, probe response deadline/body size 제한
  • React Provider와 subscription/device lifecycle Hook

후속 단계:

  • Node SSE transport, identity store와 runtime control Adapter
  • nanumi_web_vite의 기존 PageEventStream 완전 교체

소비 애플리케이션에 남을 Implementation:

  • stream key와 domain event handler 구성
  • React Query, Recoil, UI, 알림음 등 업무 side effect
  • managed device 적용 화면 결정
  • 서버 URL과 app version 주입

서버의 Nest, RxJS, Redis, MongoDB 타입이나 source를 import하지 않습니다. Client가 실제로 송수신하는 wire 계약만 이 패키지 안에서 독립적으로 정의합니다.

디렉터리

src/
├── core/      # runtime-neutral contract와 SSE lifecycle
├── browser/   # Browser Adapter
├── react/     # React 17+ Provider와 Hook
└── index.ts   # runtime-neutral entry

Node Adapter는 실제 구현을 시작할 때 별도 source와 public entrypoint를 추가합니다.

Browser 사용

import { createBrowserSseClient } from '@gaonprime/nanumi-sse-client/browser';

const client = createBrowserSseClient({
  baseUrl: 'https://relay.example.com',
  device: {
    appInfo: { name: 'nanumi-web', version: '1.0.0' },
  },
});

const unsubscribe = client.subscribe({
  streamKeys: ['pos-order:funeral-home-1'],
  handlers: {
    'pos-order.created': async (_payload, _context) => {
      // 업무 데이터 refetch는 소비 애플리케이션 책임입니다.
    },
  },
  onSyncRequired: async () => {
    // authoritative data refetch
  },
});
const releaseDevice = client.startDeviceLifecycle();

// 화면/runtime 종료 시
releaseDevice();
unsubscribe();
client.dispose();

한 runtime에서 client instance 하나를 공유해야 합니다. Package와 기존 connection manager를 동시에 실행하면 안 됩니다.

React 17+ 사용

import { createBrowserSseClient } from '@gaonprime/nanumi-sse-client/browser';
import {
  SseClientProvider,
  useDeviceLifecycle,
  useEventStreamSubscription,
} from '@gaonprime/nanumi-sse-client/react';

const sseClient = createBrowserSseClient({
  baseUrl: 'https://relay.example.com',
  device: {
    appInfo: { name: 'nanumi-web', version: '1.0.0' },
  },
});

interface PosOrderEvents {
  'pos-order.created': { orderId: string; total: number };
  'pos-order.deleted': { orderId: string };
}

export function Root() {
  return (
    <SseClientProvider client={sseClient}>
      <App />
    </SseClientProvider>
  );
}

export function OrderPage({
  refetchOrders,
}: {
  refetchOrders: () => Promise<void>;
}) {
  useEventStreamSubscription<PosOrderEvents>({
    streamKeys: ['pos-order:funeral-home-1'],
    handlers: {
      'pos-order.created': async (payload) => {
        // payload는 { orderId: string; total: number }로 추론됩니다.
        console.debug(payload.orderId, payload.total);
        await refetchOrders();
      },
    },
    onSyncRequired: refetchOrders,
  });
  // createBrowserSseClient의 device 설정이 있는 managed 화면에서만 호출합니다.
  useDeviceLifecycle();
  return null;
}

Generic event map에 없는 handler key는 TypeScript 오류가 되며, 선언된 key는 자동완성과 key별 payload 추론을 제공합니다. 같은 generic 계약은 client.subscribe<Events>()에서도 사용할 수 있습니다.

Client는 React render 밖에서 한 번 생성합니다. Provider와 Hook은 client를 dispose()하지 않으며, 전체 runtime 종료 시 composition root가 직접 정리합니다.

Heartbeat 주기 설정 (0.2.1부터)

SDK 초기화 시 device.heartbeatIntervalMs로 정기 보고 주기를 지정할 수 있습니다. 생략 시 **15분(900000ms)**이며, 60000~86400000 범위의 정수만 허용합니다.

const client = createBrowserSseClient({
  baseUrl: 'https://socket.example.com',
  device: {
    appInfo: { name: 'nanumi-web' },
    heartbeatIntervalMs: 15 * 60 * 1000,
  },
});

설정은 초기화 시 확정됩니다. 설정 API에서 읽은 값을 전달할 수 있지만 이미 생성된 client의 옵션 객체를 수정해도 반영되지 않습니다. 정상 보고 완료 후 다음 보고와 일반 정기 보고 실패 후 대기에 적용하며, 최초 등록 5초 재시도와 변경 메타의 빠른 재시도는 그대로 유지합니다. 메타 변경은 주기를 기다리지 않고 즉시 보고하며, 성공 후 설정 주기로 다시 예약합니다. 서버의 예상 보고 주기도 함께 맞추세요. 기존 60분 동작이 필요하면 3600000을 명시합니다. 이 옵션과 기본값 변경은 0.2.1부터 적용되며 0.2.0에는 적용되지 않습니다.

Device 업무 메타 (0.2.0)

아래 API는 0.2.0부터 제공합니다. 서버의 메타 지원을 먼저 반영한 뒤 사용하세요. 최초 등록부터 장례식장·호실·영상 모니터·페이지 정보를 함께 보냅니다.

import {
  createBrowserSseClient,
  type DeviceMeta,
} from '@gaonprime/nanumi-sse-client/browser';

const client = createBrowserSseClient({
  baseUrl: 'https://relay.example.com',
  device: {
    appInfo: { name: 'nanumi-web', version: '1.0.0' },
    meta: null, // 업무정보가 준비될 때까지 등록 대기. domain SSE는 동작합니다.
  },
});

// 실제 앱의 데이터 로딩 완료·라우트/모드·호실·모니터 변경 시 호출합니다.
const meta: DeviceMeta = {
  funeralHomeId: 'fh-1',
  funeralHomeName: '나누미 장례식장',
  roomId: null,
  roomName: null,
  videoMonitorId: 'monitor-1',
  videoMonitorName: '로비 영상 모니터',
  page: {
    type: 'video-monitor',
    label: '영상 모니터',
    pathname: window.location.pathname,
  },
};
client.updateDeviceMeta(meta);

등록은 기존처럼 유효 구독과 Device lifecycle lease가 모두 있을 때 시작합니다. 처음부터 정보가 준비됐다면 factory의 device.meta에 직접 전달할 수 있습니다. 이후 변경은 같은 client의 setter를 호출하세요. React에서도 client 재생성 대신 데이터가 준비된 effect에서 setter를 호출합니다. 변경만으로 SSE·session·cursor가 바뀌지 않습니다.

메타는 부분 patch가 아닌 전체 값입니다. 호실/모니터가 없으면 해당 ID와 이름을 둘 다 null로 보내며, 아직 로딩 중인 값을 null로 대체하지 않습니다. 같은 경로라도 용도·모드가 다르면 page.type을 구분하고 label은 사람이 읽을 화면 이름으로 지정하세요. pathname은 query/hash를 제외한 실제 경로여야 합니다. 경로가 맞지 않으면 보고가 보류되므로 라우트 변경 후 최신 값으로 updateDeviceMeta()를 호출해야 합니다.

Browser SDK는 등록·heartbeat·probe마다 현재 window.location.search 전체를 meta.page.search에 자동으로 채웁니다. 소비 앱에서 따로 전달할 필요가 없으며, query가 없으면 빈 문자열입니다. 순서·중복 키·인코딩을 그대로 보존하고 hash는 포함하지 않습니다. query만 바뀌면 다음 보고에서 반영됩니다. 즉시 보고하려면 query 변경을 의존성으로 둔 React effect에서 page.search: window.location.search를 포함한 최신 메타로 updateDeviceMeta()를 호출하세요. 동일 메타를 그대로 전달하면 변경 보고가 생략됩니다. type·label과 장례식장·호실 정보는 계속 소비 앱에서 설정합니다.

이 계약을 지원하는 서버를 먼저 배포한 뒤 SDK를 업데이트해야 합니다. 이전 서버의 엄격한 필드 검증은 page.search를 거부합니다. 새 서버는 search 없는 기존 메타도 허용합니다.

메타 사용 시 register·heartbeat·probe가 같은 구조를 전달하고 경로 기준은 meta.page.pathname + (meta.page.search ?? '')입니다. 중복되는 location.pathname/search와 diagnostics의 location/ visibilityState는 제거됩니다. 메타를 생략한 기존 사용은 기존 전송 형식을 유지합니다.

전체 페이지 URL (0.3.0부터)

Browser SDK는 등록·heartbeat·probe의 meta.page.url에 현재 window.location.href를 그대로 기록한다. 도메인·포트·경로·query·hash와 인코딩을 보존하며 URL을 재조합하거나 정규화하지 않는다. pathname·search는 개별 검색과 기존 계약을 위해 함께 유지한다. Probe에서는 비동기 진단 수집 전에 URL을 복사한다. 변경된 URL은 다음 보고에 반영되며, 즉시 보고하려면 최신 page.url을 포함해 updateDeviceMeta()를 호출한다.

url은 최대 8,192자의 절대 URL이며 기존 16 KiB 메타 제한도 적용한다. 초과 시 자르지 않고 검증 오류를 반환한다. 구버전 클라이언트의 필드 생략은 허용한다. page.url을 허용하는 서버를 먼저 배포한 뒤 SDK를 업데이트한다. Dashboard 상세의 전체 URL과 전체 검색 q에서 사용할 수 있다.

기존 앱에서 전환하기

  1. 모든 서버 instance와 Dashboard를 먼저 업데이트합니다. 구버전 서버는 등록을 성공으로 응답해도 메타를 무시합니다. Probe 시작/응답 수신 서버 중 한쪽이라도 구버전이면 REACHABLE이어도 메타와 경로가 유실됩니다.
  2. Probe 결과를 읽는 코드는 경로를 result.meta?.page.pathname ?? result.location?.pathname, 표시 상태와 수신 시각을 루트 visibilityState, receivedAt에서 읽도록 변경합니다.
  3. SDK와 소비 앱의 메타 연동을 배포합니다. 메타 미사용 기기는 기존 요청 형식과 4xx 이후 최초 register 5초/정기 heartbeat 설정 주기(기본 15분) 재시도 정책으로 계속 동작합니다.

SseClient를 직접 구현한 wrapper/mock에는 필수 updateDeviceMeta()를 추가해야 합니다. Factory 반환값을 그대로 사용하는 코드는 이미 메서드를 제공합니다. 실제 wrapper는 내부 client의 updateDeviceMeta(meta)로 위임하세요. 메타를 사용하지 않는 테스트 mock에만 no-op 또는 jest.fn()을 사용할 수 있습니다.

서버를 롤백하려면 먼저 소비 앱이 메타를 생략하도록 되돌리고, 이미 열린 탭도 전환됐는지 확인하세요. meta: null은 비활성화가 아니라 등록 대기이며 setter로 해제할 수 없습니다. 실행 중인 메타 사용 client는 종료한 뒤 메타를 생략한 client로 교체해야 합니다. 상세 운영 조건은 메타 호환성 계약을 따릅니다.

보안과 Probe 진단정보

installationId는 안정적인 기기 식별자일 뿐 인증 credential이 아닙니다. Server는 이 값을 사용자 인증이나 권한 부여의 근거로 사용하면 안 됩니다. Package에는 관리자 API key, device-control key 또는 로컬 인증 우회 설정이 포함되지 않습니다.

Device 설정에서 collectDiagnostics를 생략하면 수동 network probe에 한해 Browser의 URL path, User-Agent와 client hints, 화면, locale, 배터리, storage 용량, memory, network, app build 정보를 best-effort로 수집합니다. Storage key/value, cookie 값, HTTP authorization header는 읽지 않습니다. URL query는 기본 포함합니다. includeLocationSearch: false는 메타 없는 probe location과 기본 diagnostics에서만 query를 제외하며, 메타의 page.search에는 항상 전체 query가 포함됩니다. Rich diagnostics가 필요하지 않으면 명시적으로 비활성화합니다.

const client = createBrowserSseClient({
  baseUrl: 'https://relay.example.com',
  device: {
    appInfo: { name: 'nanumi-web', version: '1.0.0' },
    collectDiagnostics: false,
    // includeLocationSearch: false, // 기존 probe 진단에서만 query 제외
  },
});

Register, heartbeat와 Probe callback HTTP 요청은 Browser credential을 포함하지 않습니다. Native EventSource는 Browser 정책에 따라 same-origin cookie를 전송할 수 있으므로 Server는 이를 installation 인증으로 간주해서는 안 됩니다. 운영 baseUrl은 HTTPS만 허용하며 HTTP는 loopback 개발환경에서만 허용합니다. Remote redirect도 same-origin root-relative path로 제한합니다.

License

MIT

변경 내역은 CHANGELOG.md를 참고합니다.

개발 명령

yarn --cwd packages/nanumi-sse-client typecheck
yarn --cwd packages/nanumi-sse-client test
yarn --cwd packages/nanumi-sse-client test:react-compat
yarn --cwd packages/nanumi-sse-client build
yarn --cwd packages/nanumi-sse-client pack:dry-run

일반 단위 테스트는 최소 지원 버전인 React 17을 사용합니다. test:react-compat는 빌드된 tarball을 격리된 소비자에 설치한 뒤 React 17, 18, 19에서 Provider와 Hook의 mount/cleanup을 각각 실행합니다. 새 stable React major가 배포되면 최신 버전도 자동 검증하며, 해당 major를 고정 matrix에 추가한 뒤 패키지를 릴리스합니다.

Local Compose의 Browser client는 source를 mount하지 않습니다. Package를 build하고 npm pack으로 만든 tarball의 ESM 산출물만 nginx image에 넣어 실행합니다.

Dashboard 관리자 열람 모드

Dashboard의 URL 열기는 저장된 전체 HTTP(S) URL을 새 탭에서 열고 __nanumi_mode=dashboard-preview query를 추가한다. 기존 query·hash는 보존하며, noopener noreferrer로 탭을 분리한다. URL이 없거나 열 수 없는 형식이면 버튼을 비활성화한다.

SDK는 초기화 시 flag 또는 해당 탭 sessionStorage에 저장된 모드를 읽는다. 최초 등록, heartbeat, probe의 최상위 runtime.kind에 dashboard-preview를 전송한다. 일반 실행은 standard이며 서버는 필드 없는 구버전 기기도 일반 필터에 포함한다.

관리자 열람에서는 별도 sessionStorage 키에 installationId를 저장한다. 같은 origin의 운영 localStorage ID를 읽거나 덮어쓰지 않으며, 새로고침 후에는 열람용 ID를 재사용하고 sessionId는 새로 생성한다. query가 사라지는 SPA 이동 후에도 탭의 열람 모드를 유지한다. 저장소 사용이 차단된 경우 현재 SDK 인스턴스의 메모리에서만 모드와 ID를 유지한다.

Dashboard는 관리자 열람 배지와 실행 구분: 전체 / 일반 / 관리자 열람 필터를 제공한다. 검색 API는 runtimeKind=standard|dashboard-preview를 받는다. 연결·등록·이벤트 수신과 기기 제어는 정상 동작하며, 이 모드는 출처 표시일 뿐 인증이나 읽기 전용을 보장하지 않는다. 서버를 먼저 배포한 뒤 SDK를 포함한 소비 앱을 업데이트해야 자동 식별된다.