Claude-skill-registry 76-webgl-ws-polling-bridge

76-webgl-ws-polling-bridge

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/76-webgl-ws-polling-bridge" ~/.claude/skills/majiayu000-claude-skill-registry-76-webgl-ws-polling-bridge && rm -rf "$T"
manifest: skills/data/76-webgl-ws-polling-bridge/SKILL.md
source content

76-webgl-ws-polling-bridge

Status: ACTIVE AppliesTo: v10 Type: Policy / Contract


Purpose

WebGL에서 WebSocket 이벤트를 폴링(Polling) 기반으로 전달하는

.jslib
↔ C# 간 계약을 정의한다.

이 문서는 인터페이스 계약이 핵심이며, 구현 코드가 정답이다.


Hard Rules

  1. SendMessage/콜백 기반 설계 금지

    • WebGL에서 WebSocket 이벤트를 C#으로 전달할 때
      SendMessage
      콜백을 사용하지 않는다.
    • 모든 이벤트는
      WS_PollEvent
      로만 소비한다.
  2. 이벤트 소비는 Tick()에서만 수행

    • Tick()
      호출 시에만 이벤트 큐를 drain한다.
    • 메인 스레드 보장.
  3. 이벤트 처리량 제한 필수

    • Tick()
      에서 최대 N개의 이벤트만 처리한다.
    • 무한 루프/폭주 방지.

Function Contract (C# → JS)

아래 함수들은

.jslib
에서 구현하고, C#에서
[DllImport("__Internal")]
로 호출한다.

WS_Connect

[DllImport("__Internal")]
private static extern int WS_Connect(string url, string subProtocolsJson);
  • 설명: WebSocket 연결 시작
  • 파라미터:
    • url
      : WebSocket URL (wss:// 필수, WebGL에서 ws:// 금지)
    • subProtocolsJson
      : sub-protocol 배열의 JSON 문자열 (nullable, 예:
      ["devian.v1"]
      )
  • 리턴:
    socketId
    (0 이상), 실패 시 음수

WS_GetState

[DllImport("__Internal")]
private static extern int WS_GetState(int socketId);
  • 설명: WebSocket 연결 상태 조회
  • 리턴:
    0=CONNECTING
    ,
    1=OPEN
    ,
    2=CLOSING
    ,
    3=CLOSED
    ,
    -1=INVALID

WS_SendBinary

[DllImport("__Internal")]
private static extern int WS_SendBinary(int socketId, IntPtr ptr, int len);
  • 설명: 바이너리 메시지 전송
  • 파라미터:
    • ptr
      : WASM heap 상의 버퍼 포인터 (
      GCHandle.Alloc(Pinned)
      )
    • len
      : 버퍼 길이
  • 리턴:
    0=성공
    , 음수=실패

WS_Close

[DllImport("__Internal")]
private static extern void WS_Close(int socketId, int code, string reason);
  • 설명: WebSocket 종료 요청
  • 파라미터:
    • code
      : 종료 코드 (예: 1000)
    • reason
      : 종료 사유 (nullable)

WS_PollEvent

[DllImport("__Internal")]
private static extern int WS_PollEvent(
    int socketId,
    out int eventType,
    out int code,
    out IntPtr dataPtr,
    out int dataLen,
    out IntPtr messagePtr);
  • 설명: 이벤트 큐에서 다음 이벤트를 가져온다
  • 리턴:
    • 0
      = 이벤트 없음
    • 1
      = 이벤트 있음 (출력값 유효)
  • 출력 파라미터:
    • eventType
      : 이벤트 타입 (아래 참조)
    • code
      : CLOSE 이벤트의 종료 코드, ERROR/MESSAGE에서는 0
    • dataPtr
      : MESSAGE 이벤트의 바이너리 데이터 포인터 (_malloc 할당)
    • dataLen
      : MESSAGE 이벤트의 데이터 길이
    • messagePtr
      : ERROR/CLOSE 이벤트의 UTF8 문자열 포인터 (_malloc 할당, nullable)

WS_FreeBuffer

[DllImport("__Internal")]
private static extern void WS_FreeBuffer(IntPtr ptr);
  • 설명: MESSAGE 이벤트의
    dataPtr
    해제
  • 필수: MESSAGE 수신 후 반드시 호출

WS_FreeString

[DllImport("__Internal")]
private static extern void WS_FreeString(IntPtr ptr);
  • 설명: ERROR/CLOSE 이벤트의
    messagePtr
    해제
  • 필수: messagePtr이 non-null이면 반드시 호출

EventType Enum

이름설명
0NONE이벤트 없음 (리턴값 0과 동일)
1OPEN연결 성공
2CLOSE연결 종료 (code, messagePtr 유효)
3ERROR오류 발생 (messagePtr 유효)
4MESSAGE바이너리 메시지 수신 (dataPtr, dataLen 유효)

WS_PollEvent 사용 패턴

// Tick() 내부
const int MaxEventsPerTick = 64;

for (int i = 0; i < MaxEventsPerTick; i++)
{
    int result = WS_PollEvent(_socketId, out int eventType, out int code,
                              out IntPtr dataPtr, out int dataLen, out IntPtr messagePtr);
    if (result == 0)
        break; // 이벤트 없음

    switch (eventType)
    {
        case 1: // OPEN
            EnqueueDispatch(() => OnOpen?.Invoke());
            break;

        case 2: // CLOSE
            string reason = messagePtr != IntPtr.Zero
                ? Marshal.PtrToStringUTF8(messagePtr)
                : "";
            EnqueueDispatch(() => OnClose?.Invoke((ushort)code, reason));
            if (messagePtr != IntPtr.Zero) WS_FreeString(messagePtr);
            break;

        case 3: // ERROR
            string errorMsg = messagePtr != IntPtr.Zero
                ? Marshal.PtrToStringUTF8(messagePtr)
                : "Unknown error";
            EnqueueDispatch(() => OnError?.Invoke(new Exception(errorMsg)));
            if (messagePtr != IntPtr.Zero) WS_FreeString(messagePtr);
            break;

        case 4: // MESSAGE
            // ArrayPool로 복사 후 core.OnFrame 호출
            var buffer = ArrayPool<byte>.Shared.Rent(dataLen);
            Marshal.Copy(dataPtr, buffer, 0, dataLen);
            _core.OnFrame(_sessionId, buffer.AsSpan(0, dataLen));
            ArrayPool<byte>.Shared.Return(buffer);
            WS_FreeBuffer(dataPtr);
            break;
    }
}

DoD (Hard Gate)

  • WS_PollEvent
    가 유일한 이벤트 소비 경로이다 (SendMessage 없음)
  • MESSAGE 수신 후
    WS_FreeBuffer(dataPtr)
    호출됨
  • ERROR/CLOSE 수신 후 messagePtr이 non-null이면
    WS_FreeString(messagePtr)
    호출됨
  • 한 Tick에서 처리하는 이벤트 개수에 상한이 있음

Reference