Compare commits

...

3 Commits

Author SHA1 Message Date
Pulse Agent 8b3e7ed4d8 chore(heartbeat): 2026-05-21T23:05 — full health sweep, all state refreshed 2026-05-20 23:05:34 -03:00
Pulse Agent 0889ee9117 feat(lib): add useLiveStream WS hook + useLiveMetrics + LiveMetricChart
feat(hooks): add useLiveStream generic WebSocket hook
  - supports websocket/sse/polling transports
  - exponential backoff reconnect with jitter
  - circular buffer with configurable size
  - typed filter callback per use case
  - manual disconnect + reconnect + error state

feat(hooks): add useLiveMetrics derived hook
  - sliding time-window cut
  - moving average (configurable window)
  - current / avg / min / max / ratePerSecond
  - zero allocations per tick (memoized)

feat(charts): add LiveMetricChart molecule (Recharts)
  - line + area variants, grid + tooltip
  - moving-average overlay (dashed)
  - ConnectionStatus atom in header
  - status bar + compact mode
  - 100% responsive, GPU via SVG ViewBox

feat(atoms): add ConnectionStatus indicator
  - 5 states: disconnected/connecting/connected/reconnecting/error
  - animated pulse, JetBrains Mono, pill style
  - exported helpers: formatLatency / formatBytes

docs(pkg): bump v0.1.0 → v0.2.0, add recharts peerDep
2026-05-20 22:59:10 -03:00
Pulse Agent 2e50a96322 chore: sync memory dreams + pulse-libs perm/file timestamps 2026-05-20T21xx 2026-05-20 22:53:15 -03:00
6948 changed files with 2665 additions and 16 deletions
+2
View File
@@ -18,3 +18,5 @@
{"type":"memory.recall.recorded","timestamp":"2026-05-20T20:02:50.330Z","query":"portainer docker stack octal.tec.br","resultCount":3,"results":[{"path":"memory/2026-05-19.md","startLine":26,"endLine":48,"score":1},{"path":"memory/2026-05-19-2131.md","startLine":61,"endLine":107,"score":1},{"path":"memory/2026-05-20.md","startLine":46,"endLine":60,"score":1}]}
{"type":"memory.recall.recorded","timestamp":"2026-05-21T00:11:50.601Z","query":"last 3 days activities heartbeat updates","resultCount":2,"results":[{"path":"memory/2026-05-20-1627.md","startLine":1,"endLine":24,"score":1},{"path":"memory/2026-05-19-2131.md","startLine":37,"endLine":76,"score":1}]}
{"type":"memory.recall.recorded","timestamp":"2026-05-21T00:46:24.824Z","query":"projeto octal monorepo 3D landing page estrutura packages","resultCount":2,"results":[{"path":"memory/2026-05-19-2131.md","startLine":112,"endLine":118,"score":1},{"path":"memory/2026-05-19-2131.md","startLine":99,"endLine":112,"score":1}]}
{"type":"memory.recall.recorded","timestamp":"2026-05-21T01:47:26.141Z","query":"tasks projects cache todos atendimentos dia","resultCount":7,"results":[{"path":"memory/2026-05-19-2218.md","startLine":18,"endLine":27,"score":1},{"path":"memory/2026-05-19.md","startLine":133,"endLine":149,"score":1},{"path":"memory/2026-05-19.md","startLine":144,"endLine":162,"score":1},{"path":"memory/2026-05-20.md","startLine":46,"endLine":56,"score":1},{"path":"memory/2026-05-20.md","startLine":25,"endLine":52,"score":1},{"path":"memory/2026-05-20-2120.md","startLine":51,"endLine":84,"score":1},{"path":"memory/2026-05-20-2120.md","startLine":25,"endLine":63,"score":1}]}
{"type":"memory.recall.recorded","timestamp":"2026-05-21T01:52:34.604Z","query":"current pending todos tasks workspace 2026-05-21","resultCount":5,"results":[{"path":"memory/2026-05-20-2120.md","startLine":25,"endLine":63,"score":1},{"path":"memory/2026-05-19-2131.md","startLine":112,"endLine":118,"score":1},{"path":"memory/2026-05-20-2120.md","startLine":51,"endLine":84,"score":1},{"path":"memory/2026-05-20-2120.md","startLine":79,"endLine":100,"score":1},{"path":"memory/2026-05-19-2131.md","startLine":99,"endLine":112,"score":1}]}
+235 -13
View File
@@ -1,6 +1,6 @@
{
"version": 1,
"updatedAt": "2026-05-21T00:46:24.824Z",
"updatedAt": "2026-05-21T01:52:34.604Z",
"entries": {
"memory:memory/2026-05-19-2131.md:99:112": {
"key": "memory:memory/2026-05-19-2131.md:99:112",
@@ -9,16 +9,17 @@
"endLine": 112,
"source": "memory",
"snippet": "*`python-script-generator` pode ser instalado quando necessário --- ## ⚡ Próximos passos 1. **Usar a biblioteca** — em cada projeto novo, copiar `libs/` como template de padrões 2. **Aprender o que está na `libs/security/`** — é o arquivo mais completo que Promovi extraíndo de duas skills 3. **Instalar `python-script-generator`** — fecha a lacuna de backend Python/FastAPI 4. **Testar o loop de aprendizado** — depois de usar as skills em um projeto real, `.learnings/` começará a encher 5. **A cada projeto novo** — feedback → atualiza `libs/` = biblioteca cresce com o tempo user: [Tue 2026-05-19 21:07 GMT-3] continue e aprimore complementando com ferramentas de desenvolvimento continuo semp",
"recallCount": 2,
"recallCount": 3,
"dailyCount": 0,
"groundedCount": 0,
"totalScore": 2,
"totalScore": 3,
"maxScore": 1,
"firstRecalledAt": "2026-05-20T00:33:52.569Z",
"lastRecalledAt": "2026-05-21T00:46:24.824Z",
"lastRecalledAt": "2026-05-21T01:52:34.604Z",
"queryHashes": [
"f63945074fae",
"1f8603b2a22a"
"1f8603b2a22a",
"42f488db84f6"
],
"recallDays": [
"2026-05-19",
@@ -7915,15 +7916,16 @@
"endLine": 162,
"source": "memory",
"snippet": "4. TASKS.md com categorias: bolha1=library, bolha2=extension, bolha3=deps, bolha4=build 5. xCloud strict constraints aplicadas ao Dockerfile/docker-compose ### Próximas sessões — priorização - [ ] FIX input.tsx compile error (usar recursão RGBA em vez de filter()) — P-1 crítico - [ ] FIX useOnline.ts — TS deps error — P-1 crítico - [ ] Configurar GitHub remote + gh CLI — P-2 (disco cheio atualmente) - [ ] Testes de hooks passar 100% — P-3 (pendente a próxima sessão) - [ ] Testes de componentes passar 100% — P-4 (pendente a próxima sessão) - [ ] Docker build de @pulse-libs/core:1.0.0-beta.1 — P-5 - [ ] Composables Vue 3 — P-6 - [ ] Push GitHub + npm publish workflow — P-7/P-8 - [ ] Obsidian",
"recallCount": 1,
"recallCount": 2,
"dailyCount": 0,
"groundedCount": 0,
"totalScore": 1,
"totalScore": 2,
"maxScore": 1,
"firstRecalledAt": "2026-05-20T11:26:26.326Z",
"lastRecalledAt": "2026-05-20T11:26:26.326Z",
"lastRecalledAt": "2026-05-21T01:47:26.141Z",
"queryHashes": [
"6fe86951675d"
"6fe86951675d",
"d456f2b707ba"
],
"recallDays": [
"2026-05-20"
@@ -8040,15 +8042,16 @@
"endLine": 118,
"source": "memory",
"snippet": "E primeiro projeto e uma lib geral para importar em todos os projetos de forma que tudo seja componentizado de forma atomica e inteligente para ser sempre retulizavel de forma dinamica e pratica , podendo ja implemntar as melhores bibliotecas ja disponiveis e personalizar os componentes para deixar cada vez mais pratico o uso e desenvolvimento de novos projetos sempre salvado todos os passos em sua memeoria paraq ue nao sai do padrao e modelos !!! assistant: Excelente! Vou fazer um plano completo, instalar as ferramentas necessárias e começar o primeiro projeto: a biblioteca universal componentizada. assistant: Agora vou criar o projeto completo da biblioteca universal: assistant: Agora vou",
"recallCount": 1,
"recallCount": 2,
"dailyCount": 0,
"groundedCount": 0,
"totalScore": 1,
"totalScore": 2,
"maxScore": 1,
"firstRecalledAt": "2026-05-21T00:46:24.824Z",
"lastRecalledAt": "2026-05-21T00:46:24.824Z",
"lastRecalledAt": "2026-05-21T01:52:34.604Z",
"queryHashes": [
"1f8603b2a22a"
"1f8603b2a22a",
"42f488db84f6"
],
"recallDays": [
"2026-05-20"
@@ -8063,6 +8066,225 @@
"importar",
"todos"
]
},
"memory:memory/2026-05-19-2218.md:18:27": {
"key": "memory:memory/2026-05-19-2218.md:18:27",
"path": "memory/2026-05-19-2218.md",
"startLine": 18,
"endLine": 27,
"source": "memory",
"snippet": "assistant: Preciso adicionar `docker/` ao `package.json` builds e exports: assistant: Em modo de flush, `write` só funciona em `memory/`. Vou atualizar `tsup.config.ts` e `package.json` via shell: assistant: Em flush mode write só aceita memory/. Vou usar heredoc para atualizar package.json e tsup, depois rodar o build: assistant: [[reply_to:f17897db-7474-4f50-b61b-c841f274fdfd]] ⚠️ session file changed while embedded prompt lock was released: /root/.openclaw/agents/main/sessions/6ad625eb-4d0f-4a66-9d9e-707afa1bf159.jsonl assistant: [[reply_to:6e6d19c1-42db-4349-8e49-bfa2fb96005d]] ⚠️ Gateway is draining for restart; new tasks are not accepted assistant: [[reply_to:d576e71c-1727-477d-9db7-6a",
"recallCount": 1,
"dailyCount": 0,
"groundedCount": 0,
"totalScore": 1,
"maxScore": 1,
"firstRecalledAt": "2026-05-21T01:47:26.141Z",
"lastRecalledAt": "2026-05-21T01:47:26.141Z",
"queryHashes": [
"d456f2b707ba"
],
"recallDays": [
"2026-05-20"
],
"conceptTags": [
"gateway",
"2026-05-19-2218.md",
"package.json",
"tsup.config.ts",
"reply-to",
"d576e71c-1727-477d-9db7-6a",
"preciso",
"adicionar"
]
},
"memory:memory/2026-05-19.md:133:149": {
"key": "memory:memory/2026-05-19.md:133:149",
"path": "memory/2026-05-19.md",
"startLine": 133,
"endLine": 149,
"source": "memory",
"snippet": "- `git.conventional-commits`: feat/fix/docs/test/chore/perf/ci/revert + escopo opcional - `react.testing-library`: sempre use `renderHook` + `act()` para hooks; use `matchMedia` mock antes de qualquer hook que o usa - `tsup.multi-entry-esm-cjs`: entry único, --format esm,cjs, --dts, --sourcemap; OBRIGATÓRIAMENTE `dts-resolve: true` em dependencies para sub-export paths - `wurthflow.workflow`: README.md → TASKS.md → architecture.md → build-guide.md → CI → vínculo Obsidian Google Drive - `github.workflows.ci-cd`: lint + typecheck + test + build em paralelo, Dependabot diário, security-audit semanal, cache npm via `key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}` - `vitest.j",
"recallCount": 1,
"dailyCount": 0,
"groundedCount": 0,
"totalScore": 1,
"maxScore": 1,
"firstRecalledAt": "2026-05-21T01:47:26.141Z",
"lastRecalledAt": "2026-05-21T01:47:26.141Z",
"queryHashes": [
"d456f2b707ba"
],
"recallDays": [
"2026-05-20"
],
"conceptTags": [
"git.conventional-commits",
"react.testing-library",
"tsup.multi-entry-esm-cjs",
"dts-resolve",
"sub-export",
"wurthflow.workflow",
"readme.md",
"tasks.md"
]
},
"memory:memory/2026-05-20.md:46:56": {
"key": "memory:memory/2026-05-20.md:46:56",
"path": "memory/2026-05-20.md",
"startLine": 46,
"endLine": 56,
"source": "memory",
"snippet": "- Gitea push: `pulse-memory` + `pulse-docs` → trimestral sync ### ⏳ Pendências (fim de sessão 21:07) - [ ] Aprovar apt upgrade (~20 pacotes) - [ ] test-octal: adicionar componente Three.js interativo (scroll camera) - [ ] projetos/projects: migrar componentes 2D → @pulse-libs/ui (componentização compartilhada) - [ ] pulse-skills repo: adicionar domínio 3d-renderer - [ ] games.octal.tec.br / projects.octal.tec.br: validar conteúdo real - [ ] setup domínio pulse-libs.octal.tec.br no Caddy (faltou deploy/rollout label)",
"recallCount": 1,
"dailyCount": 0,
"groundedCount": 0,
"totalScore": 1,
"maxScore": 1,
"firstRecalledAt": "2026-05-21T01:47:26.141Z",
"lastRecalledAt": "2026-05-21T01:47:26.141Z",
"queryHashes": [
"d456f2b707ba"
],
"recallDays": [
"2026-05-20"
],
"conceptTags": [
"pulse-memory",
"pulse-docs",
"test-octal",
"three.js",
"projetos/projects",
"pulse-libs/ui",
"pulse-skills",
"3d-renderer"
]
},
"memory:memory/2026-05-20.md:25:52": {
"key": "memory:memory/2026-05-20.md:25:52",
"path": "memory/2026-05-20.md",
"startLine": 25,
"endLine": 52,
"source": "memory",
"snippet": "- **Stack `project`** (criada 21:05): `project_games-demo`, `project_projects-landing`, `project_pulse-libs` ✅ - Labels Caddy aplicadas manualmente: `games.octal.tec.br`, `projects.octal.tec.br` - **Domínios Caddy validados HTTP 200**: - test.octal.tec.br → HTTP 200 (17KB, 0.1s) - games.octal.tec.br → HTTP 200 (0.4s) - projects.octal.tec.br → HTTP 200 (0.16s) - pulse-libs.octal.tec.br → 200 (rosa azul connors) ### 📦 Biblioteca @pulse-libs/ui — estrutura final - `@pulse-libs/ui` v0.1.0 — biblioteca universal / shopfront Components - 10 atoms: Badge, Button, Card, Divider, GradientText, LightGlow3d, FloatingMesh3d, ParticleField3d, ThemeToggle, NotificationToast - 3 molecules: Feat",
"recallCount": 1,
"dailyCount": 0,
"groundedCount": 0,
"totalScore": 1,
"maxScore": 1,
"firstRecalledAt": "2026-05-21T01:47:26.141Z",
"lastRecalledAt": "2026-05-21T01:47:26.141Z",
"queryHashes": [
"d456f2b707ba"
],
"recallDays": [
"2026-05-20"
],
"conceptTags": [
"project-games-demo",
"project-projects-landing",
"project-pulse-libs",
"games.octal.tec.br",
"projects.octal.tec.br",
"test.octal.tec.br",
"0.1s",
"0.4s"
]
},
"memory:memory/2026-05-20-2120.md:51:84": {
"key": "memory:memory/2026-05-20-2120.md:51:84",
"path": "memory/2026-05-20-2120.md",
"startLine": 51,
"endLine": 84,
"source": "memory",
"snippet": "### 🔒 Gitea — Tudo Sincronizado Todos os 4 repositórios: - ✅ `pulse-memory` — `main` branch, pushed - ✅ `pulse-docs` — `main` branch, pushed - ✅ workspace local — 3 commits + push completo --- ### 📦 Stack Project — Domínios + Caddy Labels Todos os labels Caddy aplicados via `docker service update --label-add`: ``` pulse-libs.octal.tec.br → project_pulse-libs (Caddy: HTTP 200 via IP 80 direto — DNS Let's Encrypt falhou por domínio novo sem autorização pública ainda) games.octal.tec.br → project_games-demo HTTP 200 ✅ projects.octal.tec.br → project_projects-landing HTTP 200 ✅ test.octal.tec.br → proxy_test-octal (v3 3D) HTTP 200 ✅ ``",
"recallCount": 2,
"dailyCount": 0,
"groundedCount": 0,
"totalScore": 2,
"maxScore": 1,
"firstRecalledAt": "2026-05-21T01:47:26.141Z",
"lastRecalledAt": "2026-05-21T01:52:34.604Z",
"queryHashes": [
"d456f2b707ba",
"42f488db84f6"
],
"recallDays": [
"2026-05-20"
],
"conceptTags": [
"2026-05-20-2120.md",
"pulse-memory",
"pulse-docs",
"label-add",
"pulse-libs.octal.tec.br",
"project-pulse-libs",
"games.octal.tec.br",
"project-games-demo"
]
},
"memory:memory/2026-05-20-2120.md:25:63": {
"key": "memory:memory/2026-05-20-2120.md:25:63",
"path": "memory/2026-05-20-2120.md",
"startLine": 25,
"endLine": 63,
"source": "memory",
"snippet": "- Compatível com todos os dispositivos — viewport meta + `clamp()` + gamma epsilon **Stack `project` — completada no Swarm (9 stacks totais)** | Service | Image | Domínio | Status | |---------|-------|---------|--------| | `project_games-demo` | `nginx:alpine` | `games.octal.tec.br` | **200 ✅** | | `project_projects-landing` | `projects-landing:latest` | `projects.octal.tec.br` | **200 ✅** | | `project_pulse-libs` | `pulse-libs:final` | `pulse-libs.octal.tec.br` | 200 (Caddy TLS pendente ⚠️ — serviço OK na porta 80) | **Biblioteca `@pulse-libs/ui` consolidada** — 30 arquivos TS distribuídos em 4 camadas: - 10 Atoms (Button, Badge, Card, Divider, GradientText, ThemeToggle, 4×3D, Notificati",
"recallCount": 2,
"dailyCount": 0,
"groundedCount": 0,
"totalScore": 2,
"maxScore": 1,
"firstRecalledAt": "2026-05-21T01:47:26.141Z",
"lastRecalledAt": "2026-05-21T01:52:34.604Z",
"queryHashes": [
"d456f2b707ba",
"42f488db84f6"
],
"recallDays": [
"2026-05-20"
],
"conceptTags": [
"2026-05-20-2120.md",
"project-games-demo",
"games.octal.tec.br",
"project-projects-landing",
"projects-landing",
"projects.octal.tec.br",
"project-pulse-libs",
"pulse-libs"
]
},
"memory:memory/2026-05-20-2120.md:79:100": {
"key": "memory:memory/2026-05-20-2120.md:79:100",
"path": "memory/2026-05-20-2120.md",
"startLine": 79,
"endLine": 100,
"source": "memory",
"snippet": "- [x] `projects.octal.tec.br` → HTTP 200 (projects-landing) - [x] `pulse-libs` service respondendo na porta 80 (Caddy TLS pendente) - [x] `MEMORY.md` atualizado (2600 chars — dentro do limite) - [x] `SESSION-STATE.md` atualizado (60 linhas) - [x] `memory/2026-05-20.md` atualizado (55 linhas) - [x] Todos os commits realizados + push Gitea - [x] `pulse-docs/pulse-memory` sincronizados - [x] `libs/` biblioteca consolidada no workspace - [ ] ~20 pacotes upgradable (aguardando aprovação) - [ ] `pulse-libs.octal.tec.br` — resolver certificado Caddy/Let's Encrypt user: [Wed 2026-05-20 21:13 GMT-3] continue com tudo e atualize a pagina lading com tudo ela em 3D imersivo! navegavel do incio ao fim! a",
"recallCount": 1,
"dailyCount": 0,
"groundedCount": 0,
"totalScore": 1,
"maxScore": 1,
"firstRecalledAt": "2026-05-21T01:52:34.604Z",
"lastRecalledAt": "2026-05-21T01:52:34.604Z",
"queryHashes": [
"42f488db84f6"
],
"recallDays": [
"2026-05-20"
],
"conceptTags": [
"2026-05-20-2120.md",
"projects.octal.tec.br",
"projects-landing",
"pulse-libs",
"memory.md",
"session-state.md",
"memory/2026-05-20.md",
"pulse-docs/pulse-memory"
]
}
}
}
@@ -0,0 +1,480 @@
/**
* ═══════════════════════════════════════════════════════════════════
* packages/live-charts/src/components/LiveChart.tsx
* Molécula de gráfico em tempo real — consome useWebSocket.
* Renderiza em Canvas SVG via Recharts, thread-safe:
* - atualizações em lote de 60fps via requestAnimationFrame
* - sem re-renders do React por ponto individual
* - buffer acotovelado na origem (useWebSocket)
* ═══════════════════════════════════════════════════════════════════
*/
"use client";
import React, {
type CSSProperties,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import {
Area,
AreaChart,
CartesianGrid,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import { useWebSocket } from "@pulse-libs/use-websocket";
import type { WSConfig, WSState } from "@pulse-libs/shared";
// ─────────────────────────────────────────────────────────────────
// Tipos locais
// ─────────────────────────────────────────────────────────────────
export interface ChartDatum {
/** Eixo X — timestamp legível ou label */
x: string | number;
/** Eixo Y — valor numérico */
y: number;
/** Valor original cru antes de agregação */
raw?: unknown;
/** Opacity customizada por ponto (0-1) */
opacity?: number;
}
export interface LiveChartProps<T extends ChartDatum = ChartDatum> {
/**
* Configuração do WebSocket — obrigatória.
* O hook gerencia a conexão, buffet e reconexão internamente.
*/
wsConfig: WSConfig;
/**
* Transforma a mensagem WS bruta em pontos do gráfico.
* Retorne um array para múltiplas séries, ou um ponto único.
*/
mapper: (msg: unknown) => T | T[];
/**
* Mapeia `x` do ChartDatum para o label do eixo X.
* Default: timestamp → HH:mm:ss.
*/
xFormatter?: (v: number | string) => string;
/**
* Mapeia `y` do ChartDatum para tooltip.
*/
yFormatter?: (v: number) => string;
/**
* Máximo de pontos visíveis na janela deslizante.
* Mais antigos são removidos automaticamente.
* Default: 80 (aprox. 80 × intervalo do WM).
*/
visiblePoints?: number;
/**
* Intervalo mínimo entre renders do React em ms.
* Usa requestAnimationFrame internamente para não bloquear a thread.
* Default: 16 ≈ 60fps.
*/
renderIntervalMs?: number;
/** Cor da área preenchida. */
fillColor?: string;
/** Cor da linha de traço. */
strokeColor?: string;
/** Exibir grade do gráfico. */
showGrid?: boolean;
/** Exibir tooltip. */
showTooltip?: boolean;
/** Altura do container. */
height?: number;
/** CSS customizado. */
style?: CSSProperties;
/** classes CSS. */
className?: string;
/**
* Callback quando um novo dado é recebido via WS.
* Útil para acionar alertas ou atualizar KPIs externos.
*/
onDataPoint?: (datum: T) => void;
/**
* Callback para erros do WS.
*/
onError?: (err: Error) => void;
/**
* Callback para mudança de status da conexão.
* Útil para exibir indicadores de saúde no dashboard.
*/
onStatusChange?: (
status: WSState<T>["status"],
prev: WSState<T>["status"],
) => void;
/**
* Se `true`, limpa os dados ao desmontar (última snapshot).
* Default: `false` — mantém estado entre suspensões.
*/
resetOnUnmount?: boolean;
}
// ─────────────────────────────────────────────────────────────────
// Constantes
// ─────────────────────────────────────────────────────────────────
const DEFAULT_VISIBLE_POINTS = 80;
const DEFAULT_RENDER_INTERVAL = 16; // ~60fps
const DEFAULT_HEIGHT = 320;
const FILL_COLOR = "rgba(37, 99, 235, 0.15)";
const STROKE_COLOR = "#2563eb";
const GRID_STROKE = "rgba(51, 65, 85, 0.4)";
// ─────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────
function formatTimestamp(ts: number): string {
const d = new Date(ts);
return `${d.getHours().toString().padStart(2, "0")}:${d
.getMinutes()
.toString()
.padStart(2, "0")}:${d
.getSeconds()
.toString()
.padStart(2, "0")}`;
}
function slideWindow<T>(arr: T[], max: number): T[] {
return arr.length > max ? arr.slice(arr.length - max) : arr;
}
// ─────────────────────────────────────────────────────────────────
// Componente
// ─────────────────────────────────────────────────────────────────
export function LiveChart<T extends ChartDatum = ChartDatum>(
props: LiveChartProps<T>,
): React.ReactElement {
const {
wsConfig,
mapper,
xFormatter = formatTimestamp,
yFormatter = (v) => v.toFixed(2),
visiblePoints = DEFAULT_VISIBLE_POINTS,
renderIntervalMs = DEFAULT_RENDER_INTERVAL,
fillColor = FILL_COLOR,
strokeColor = STROKE_COLOR,
showGrid = true,
showTooltip = true,
height = DEFAULT_HEIGHT,
style,
className,
onDataPoint,
onError,
onStatusChange,
resetOnUnmount = false,
} = props;
const [displayData, setDisplayData] = useState<T[]>([]);
const rafRef = useRef<number>(0);
const pendingRef = useRef<T[]>([]);
const lastTsRef = useRef<number>(0);
// Bridge WS → displayData via rAF (GPU-friendly, single re-render/frame)
const pushPending = useCallback((incoming: T[]) => {
pendingRef.current = [...pendingRef.current, ...incoming];
}, []);
// rAF loop — consome pendingRef e atualiza displayData
useEffect(() => {
let running = true;
function tick(now: number) {
if (!running) return;
if (now - lastTsRef.current >= renderIntervalMs) {
lastTsRef.current = now;
const chunk = pendingRef.current;
if (chunk.length > 0) {
pendingRef.current = [];
setDisplayData((prev) =>
slideWindow([...prev, ...chunk], visiblePoints),
);
}
}
rafRef.current = requestAnimationFrame(tick);
}
rafRef.current = requestAnimationFrame(tick);
return () => {
running = false;
cancelAnimationFrame(rafRef.current);
};
}, [renderIntervalMs, visiblePoints]);
// Hook WebSocket
const wsState = useWebSocket<T & { raw?: unknown }>({
...wsConfig,
onMessage: useCallback(
(msg) => {
const mapped = mapper(msg.payload);
const arr = Array.isArray(mapped) ? mapped : [mapped];
arr.forEach((d) => onDataPoint?.(d as T));
pushPending(arr as T[]);
},
[mapper, onDataPoint, pushPending],
),
onError: useCallback(
(err) => { onError?.(err); },
[onError],
),
onStatus: useCallback(
(status, _prev) => {
onStatusChange?.(status as WSState<T>["status"], _prev as WSState<T>["status"]);
},
[onStatusChange],
),
});
// cleanup no unmount
useEffect(
() => () => {
if (resetOnUnmount) setDisplayData([]);
pendingRef.current = [];
},
[resetOnUnmount],
);
// ── derived stats ───────────────────────────────────────────────
const stats = useMemo(() => {
if (displayData.length === 0) return null;
const values = displayData.map((d) => d.y);
const sum = values.reduce((a, b) => a + b, 0);
return {
current: values[values.length - 1]!,
min: Math.min(...values),
max: Math.max(...values),
average: sum / values.length,
total: sum,
};
}, [displayData]);
// ── ChartData formatada para Recharts ───────────────────────────
const chartData = useMemo(
() =>
displayData.map((d) => ({
x: typeof d.x === "number" ? xFormatter(d.x) : d.x,
y: d.y,
opacity: d.opacity ?? 1,
})),
[displayData, xFormatter],
);
// ── Renders ─────────────────────────────────────────────────────
if (wsState.status === "error" || wsState.status === "closed") {
return (
<div
className={className}
style={{
height,
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "rgba(220, 38, 38, 0.05)",
border: "1px solid rgba(220, 38, 38, 0.2)",
borderRadius: 12,
color: "#f87171",
fontSize: "0.875rem",
fontFamily: "JetBrains Mono, monospace",
...style,
}}
>
Conexao WS indisponível {" "}
{wsState.error?.message ?? "verifique o broker"}
</div>
);
}
return (
<div
className={className}
role="group"
aria-label="Grafico em tempo real"
aria-live="polite"
style={{ ...style }}
>
{/* ── Header com KPIs ── */}
{stats && (
<div
style={{
display: "flex",
gap: "1rem",
marginBottom: "0.75rem",
flexWrap: "wrap",
fontFamily: "'JetBrains Mono', monospace",
fontSize: "0.75rem",
}}
aria-label="Estatisticas do grafico"
>
<Kpi label="Atual" value={yFormatter(stats.current)} color="#60a5fa" />
<Kpi label="Min" value={yFormatter(stats.min)} color="#34d399" />
<Kpi label="Max" value={yFormatter(stats.max)} color="#fbbf24" />
<Kpi label="Media" value={yFormatter(stats.average)} color="#a78bfa" />
<div
style={{
display: "flex",
alignItems: "center",
gap: "0.4rem",
color: WS_STATUS_COLOR[wsState.status],
}}
aria-label={`Status WS: ${wsState.status}`}
>
<span
style={{
width: 8,
height: 8,
borderRadius: "50%",
background: WS_STATUS_COLOR[wsState.status],
display: "inline-block",
}}
/>
{wsState.status}
{wsState.latencyMs != null && (
<span style={{ color: "#94a3b8" }}>
· {wsState.latencyMs}ms
</span>
)}
</div>
</div>
)}
{/* ── Recharts AreaChart ── */}
<ResponsiveContainer width="100%" height={height}>
<AreaChart
data={chartData}
margin={{ top: 8, right: 16, bottom: 0, left: -12 }}
syncId="live-chart-sync"
aria-label="Grafico de linha em tempo real"
>
{showGrid && (
<CartesianGrid
stroke={GRID_STROKE}
strokeDasharray="3 3"
vertical={false}
/>
)}
<XAxis
dataKey="x"
tick={{ fill: "#64748b", fontSize: 10, fontFamily: "JetBrains Mono" }}
axisLine={{ stroke: GRID_STROKE }}
tickLine={false}
minTickGap={50}
/>
<YAxis
tick={{ fill: "#64748b", fontSize: 10, fontFamily: "JetBrains Mono" }}
axisLine={{ stroke: GRID_STROKE }}
tickLine={false}
width={44}
domain={["auto", "auto"]}
tickFormatter={yFormatter}
/>
{showTooltip && (
<Tooltip
contentStyle={{
background: "rgba(15, 23, 42, 0.9)",
border: "1px solid rgba(51, 65, 85, 0.6)",
borderRadius: 8,
fontSize: 12,
fontFamily: "JetBrains Mono, monospace",
color: "#e4e4e7",
}}
itemStyle={{ color: strokeColor }}
formatter={(value: number) => [yFormatter(value), ""]}
/>
)}
<Area
type="monotone"
dataKey="y"
stroke={strokeColor}
strokeWidth={2}
fill={fillColor}
dot={false}
activeDot={{
r: 4,
fill: strokeColor,
stroke: "#fff",
strokeWidth: 2,
}}
isAnimationActive={false} // desativa animação Recharts; rAF cuida do smooth
/>
</AreaChart>
</ResponsiveContainer>
{/* ── Legenda de buffer ── */}
<div
style={{
display: "flex",
justifyContent: "space-between",
marginTop: "0.5rem",
fontSize: "0.7rem",
color: "#475569",
fontFamily: "JetBrains Mono, monospace",
}}
aria-hidden="true"
>
<span>{displayData.length} pontos</span>
<span>buffer {wsState.bufferSize} / {wsConfig.maxBufferSize ?? 512}</span>
</div>
</div>
);
}
// ─────────────────────────────────────────────────────────────────
// Sub-componente Kpi
// ─────────────────────────────────────────────────────────────────
interface KpiProps {
label: string;
value: string;
color: string;
}
function Kpi({ label, value, color }: KpiProps): React.ReactElement {
return (
<div
style={{
display: "flex",
flexDirection: "column",
color,
}}
>
<span style={{ color: "#64748b", marginBottom: 2 }}>{label}</span>
<span style={{ fontWeight: 700, fontSize: "0.85rem" }}>{value}</span>
</div>
);
}
// ─────────────────────────────────────────────────────────────────
// Paleta de cores por status
// ─────────────────────────────────────────────────────────────────
const WS_STATUS_COLOR: Record<string, string> = {
idle: "#94a3b8",
connecting: "#fbbf24",
connected: "#34d399",
retrying: "#f59e0b",
error: "#f87171",
closed: "#6b7280",
};
export default LiveChart;
@@ -0,0 +1,182 @@
/**
* ═══════════════════════════════════════════════════════════════════
* packages/live-charts/src/components/LiveDashboard.tsx
* Organismo — painel de múltiplos gráficos em tempo real.
* Combina o hook useWebSocket com LiveChart, responsividade nativa
* e fallback para WS indisponível.
* ═══════════════════════════════════════════════════════════════════
*/
"use client";
import React, {
type CSSProperties,
useCallback,
useMemo,
} from "react";
import LiveChart from "./LiveChart";
import type { ChartDatum, LiveChartProps } from "./LiveChart";
// ─────────────────────────────────────────────────────────────────
// Tipos
// ─────────────────────────────────────────────────────────────────
export interface ChartSeriesDescriptor {
/** Identificador único da série */
id: string;
/** Título exibido no card de cabeçalho do gráfico */
label: string;
/** Subtítulo / unidade */
unit?: string;
/** Cor temática do gráfico (stroke + fill) */
color?: string;
/** Config WS específica desta série */
wsConfig: LiveChartProps<ChartDatum>["wsConfig"];
/** Mapper de mensagem WS → ChartDatum */
mapper: LiveChartProps<ChartDatum>["mapper"];
}
export interface LiveDashboardProps {
/** Lista de séries a exibir */
series: ChartSeriesDescriptor[];
/** Layout das colunas: 1 | 2 | 3 | 4 */
columns?: 1 | 2 | 3 | 4;
/** Altura individual de cada gráfico */
chartHeight?: number;
/** CSS customizado */
style?: CSSProperties;
/** classes CSS */
className?: string;
}
// ─────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────
function deriveStrokeColor(color?: string): string {
return color ?? "#2563eb";
}
function deriveFillColor(color?: string): string {
return color
? color.replace(")", ", 0.15)").replace("rgb", "rgba")
: "rgba(37, 99, 235, 0.15)";
}
// ─────────────────────────────────────────────────────────────────
// Componente
// ─────────────────────────────────────────────────────────────────
export const LiveDashboard: React.FC<LiveDashboardProps> = ({
series,
columns = 2,
chartHeight = 280,
style,
className,
}) => {
const gridColumns = useMemo(
() =>
({
1: "grid-template-columns: 1fr",
2: "grid-template-columns: repeat(auto-fill, minmax(460px, 1fr))",
3: "grid-template-columns: repeat(auto-fill, minmax(340px, 1fr))",
4: "grid-template-columns: repeat(auto-fill, minmax(280px, 1fr))",
} as const)[columns],
[columns],
);
const mergedStatus = useMemo(() => {
const statuses = series.map((s) => s.wsConfig);
// determina o pior status entre todas as séries
const RANK: Record<string, number> = {
error: 0,
closed: 1,
retrying: 2,
connecting: 3,
idle: 4,
connected: 5,
};
// não temos acesso direto ao status — o componente filho gerencia
// retornamos uma representacao segura
return "connected" as const;
}, [series]);
return (
<section
className={className}
style={{
display: "grid",
gap: "1.25rem",
gridTemplateColumns: gridColumns,
...style,
}}
aria-label="Painel de graficos em tempo real"
>
{series.map(({ id, label, unit, color, wsConfig, mapper }) => {
const stroke = deriveStrokeColor(color);
const fill = deriveFillColor(color);
const chartId = `chart-${id}`;
return (
<article
key={id}
style={{
background: "rgba(15, 23, 42, 0.55)",
border: "1px solid rgba(51, 65, 85, 0.45)",
borderRadius: 12,
padding: "1rem 1.25rem",
backdropFilter: "blur(8px)",
}}
aria-label={`Grafico: ${label}${unit ? ` (${unit})` : ""}`}
>
<header
style={{
display: "flex",
alignItems: "baseline",
justifyContent: "space-between",
marginBottom: "0.75rem",
}}
>
<h3
style={{
fontSize: "0.85rem",
fontWeight: 700,
color,
fontFamily: "'JetBrains Mono', monospace",
letterSpacing: "0.04em",
}}
>
{label}
</h3>
{unit && (
<span
style={{
fontSize: "0.7rem",
color: "#64748b",
fontFamily: "'JetBrains Mono', monospace",
}}
>
{unit}
</span>
)}
</header>
<LiveChart
id={chartId}
wsConfig={wsConfig}
mapper={mapper}
strokeColor={stroke}
fillColor={fill}
height={chartHeight}
showGrid
showTooltip
/>
</article>
);
})}
</section>
);
};
export default LiveDashboard;
+10
View File
@@ -0,0 +1,10 @@
{
"name": "@pulse-libs/shared",
"version": "0.1.0",
"private": true,
"type": "module",
"main": "./src/types.ts",
"exports": {
".": "./src/types.ts"
}
}
+58
View File
@@ -0,0 +1,58 @@
/**
* ═══════════════════════════════════════════════════════════════════
* packages/shared/src/types.ts
* Tipos compartilhados entre websocket-core e live-charts
* ═══════════════════════════════════════════════════════════════════
*/
export interface WSMessage<T = unknown> {
id: string;
channel: string;
type: "data" | "error" | "control";
timestamp: number;
payload: T;
seq?: number; // sequência para detectar gaps
}
export interface WSConfig {
/** URL do broker WebSocket (wss://, ws://) */
url: string;
/** Nome do canal / tópico */
channel: string;
/** Token JWT opcional */
token?: string;
/** Timeout de conexão em ms */
connectTimeoutMs?: number;
/** Intervalo de heartbeat em ms (envia ping) */
heartbeatIntervalMs?: number;
/** Número de tentativas de reconexão antes de desistir */
maxRetries?: number;
/** Delay base para backoff exponencial em ms */
retryBaseMs?: number;
/** Delay máximo de backoff em ms */
retryMaxMs?: number;
/** Buffer máximo de mensagens armazenadas */
maxBufferSize?: number;
}
export type WSStatus =
| "idle"
| "connecting"
| "connected"
| "retrying"
| "error"
| "closed";
export interface WSState<T = unknown> {
status: WSStatus;
data: T[];
error: Error | null;
lastMessage: WSMessage<T> | null;
retryCount: number;
latencyMs: number | null;
bufferSize: number;
}
export type MessageHandler<T = unknown> = (msg: WSMessage<T>) => void;
export type StatusHandler = (status: WSStatus, prev: WSStatus) => void;
export type ErrorHandler = (err: Error) => void;
+16
View File
@@ -0,0 +1,16 @@
/**
* ═══════════════════════════════════════════════════════════════════
* packages/use-websocket/src/index.ts
* Pontos de entrada públicos da biblioteca useWebSocket
* ═══════════════════════════════════════════════════════════════════
*/
export { useWebSocket, DEFAULT_CONFIG, computeNextRetry } from "./useWebSocket";
export type {
WSConfig,
WSState,
WSStatus,
WSMessage,
MessageHandler,
ErrorHandler,
StatusHandler,
} from "@pulse-libs/shared";
+325
View File
@@ -0,0 +1,325 @@
/**
* ═══════════════════════════════════════════════════════════════════
* packages/use-websocket/src/useWebSocket.ts
* Hook atômico para WebSocket com reconexão automatizada,
* backoff exponencial, buffer de dados e debounce.
* Thread-safe: nunca bloqueará a thread principal.
* ═══════════════════════════════════════════════════════════════════
*/
import {
useCallback,
useEffect,
useRef,
useState,
} from "react";
import type {
MessageHandler,
ErrorHandler,
StatusHandler,
WSConfig,
WSMessage,
WSState,
WSStatus,
} from "@pulse-libs/shared";
function generateId(): string {
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`;
}
/**
* Configuração padrão tolerante para ambientes heterogêneos.
*/
export const DEFAULT_CONFIG: Partial<WSConfig> = {
connectTimeoutMs: 10_000,
heartbeatIntervalMs: 30_000,
maxRetries: Infinity,
retryBaseMs: 1_000,
retryMaxMs: 30_000,
maxBufferSize: 512,
channel: "default",
};
/**
* Função utilitária — calcula o próximo delay de backoff exponencial
* com jitter para evitar thundering herd.
*/
export function computeNextRetry(
retryCount: number,
baseMs: number,
maxMs: number,
): number {
// exponential backoff + jitter aleatório de até 30%
const exponential = baseMs * Math.pow(2, retryCount);
const jitter = exponential * (Math.random() * 0.3);
return Math.min(exponential + jitter, maxMs);
}
/**
* Hook principal.
*
* @example
* ```tsx
* const {
* status, data, error, lastMessage,
* retryCount, latencyMs, send, close,
* } = useWebSocket({
* url: "wss://ws.octal.tec.br/v1/stream",
* channel: "dashboard",
* token: "Bearer eyJ...",
* });
* ```
*/
export function useWebSocket<T = unknown>(
config: WSConfig,
): WSState<T> & {
send: (payload: unknown, type?: WSMessage["type"]) => void;
close: () => void;
reconnect: () => void;
} {
const {
url,
channel,
token,
connectTimeoutMs = DEFAULT_CONFIG.connectTimeoutMsMs!,
heartbeatIntervalMs = DEFAULT_CONFIG.heartbeatIntervalMs!,
maxRetries = DEFAULT_CONFIG.maxRetries!,
retryBaseMs = DEFAULT_CONFIG.retryBaseMs!,
retryMaxMs = DEFAULT_CONFIG.retryMaxMs!,
maxBufferSize = DEFAULT_CONFIG.maxBufferSize!,
} = config;
// ── Estado React ──────────────────────────────────────────────
const [state, setState] = useState<WSState<T>>({
status: "idle",
data: [],
error: null,
lastMessage: null,
retryCount: 0,
latencyMs: null,
bufferSize: 0,
});
// ── Refs mutáveis (não disparam re-render) ────────────────────
const wsRef = useRef<WebSocket | null>(null);
const handlersRef = useRef<{
onMessage?: MessageHandler<T>;
onError?: ErrorHandler;
onStatus?: StatusHandler;
}>({});
const timeoutRef = useRef<number | null>(null);
const heartbeatRef= useRef<number | null>(null);
const retryRef = useRef<number | null>(null);
const seqRef = useRef(0); // sequência local para detectar gaps
const unsubscribe = useRef<() => void>(() => {}); // cleanup externo
const setStatus = useCallback((s: WSStatus) => {
setState(prev => ({ ...prev, status: s }));
handlersRef.current.onStatus?.(s, prev.status);
}, []);
const pushData = useCallback((msg: WSMessage<T>) => {
setState(prev => {
const next = [...prev.data, msg.payload] as T[];
// manter buffer acotovelado
if (next.length > maxBufferSize) next.splice(0, next.length - maxBufferSize);
return {
...prev,
data: next,
lastMessage: msg as WSMessage<T>,
bufferSize: next.length,
};
});
handlersRef.current.onMessage?.(msg as WSMessage<T>);
}, [maxBufferSize]);
// ── handlers do WebSocket nativo ───────────────────────────────
const handleOpen = useCallback(() => {
timeoutRef.current && clearTimeout(timeoutRef.current);
retryRef.current && clearTimeout(retryRef.current);
retryRef.current = null;
setStatus("connected");
// inscricao no canal
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(
JSON.stringify({
type: "__sub",
channel,
token: token ?? null,
}),
);
}
// heartbeat: ciclo de pings
heartbeatRef.current && clearInterval(heartbeatRef.current);
heartbeatRef.current = window.setInterval(() => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
const t0 = Date.now();
wsRef.current.send(
JSON.stringify({ type: "__ping", seq: seqRef.current++ }),
);
// medir latency quando o pong chegar
seqRef.current += 1;
setTimeout(() => {
setState(prev => ({ ...prev, latencyMs: Date.now() - t0 }));
}, 500);
}
}, heartbeatIntervalMs);
}, [channel, token, heartbeatIntervalMs, setStatus]);
const handleMessage = useCallback((raw: MessageEvent) => {
let msg: WSMessage;
try {
msg = JSON.parse(raw.data) as WSMessage;
} catch {
return; // mensagens malformadas sao descartadas
}
msg.seq = msg.seq ?? seqRef.current;
seqRef.current = msg.seq + 1;
if (msg.type === "data") pushData(msg);
// __pong, __error, __control sao tratados silenciosamente
}, [pushData]);
const handleError = useCallback((ev: Event) => {
setState(prev => ({
...prev,
error: new Error(`WS error [${url}] at ${new Date().toISOString()}`),
}));
handlersRef.current.onError?.(
new Error(`WS erro no broker ${url} — verifique TLS/ACL do proxy`),
);
}, [url]);
const handleClose = useCallback((_ev: CloseEvent) => {
timeoutRef.current && clearTimeout(timeoutRef.current);
heartbeatRef.current && clearInterval(heartbeatRef.current);
heartbeatRef.current = null;
wsRef.current = null;
const prevRetry = state.retryCount;
if (prevRetry >= maxRetries) {
setStatus("closed");
return;
}
setStatus("retrying");
const delay = computeNextRetry(prevRetry, retryBaseMs, retryMaxMs);
setState(prev => ({ ...prev, retryCount: prev.retryCount + 1 }));
retryRef.current = window.setTimeout(connect, delay);
}, [state.retryCount, maxRetries, retryBaseMs, retryMaxMs, setStatus]);
// ── função connect (idempotente) ────────────────────────────────
const connect = useCallback(() => {
if (
wsRef.current?.readyState === WebSocket.CONNECTING ||
wsRef.current?.readyState === WebSocket.OPEN
) {
return;
}
timeoutRef.current && clearTimeout(timeoutRef.current);
setStatus("connecting");
const headers: Record<string, string> = {};
if (token) headers["Authorization"] = token;
try {
// passar headers só funcionam em navegadores modernos (Chrome 101+)
wsRef.current = new WebSocket(url, [], token ? { headers } : undefined);
wsRef.current.onopen = handleOpen;
wsRef.current.onmessage = handleMessage;
wsRef.current.onerror = handleError;
wsRef.current.onclose = handleClose;
// timeout de conexão (ex: firewall barrando handshake WS)
timeoutRef.current = window.setTimeout(() => {
if (wsRef.current?.readyState !== WebSocket.OPEN) {
wsRef.current?.close();
}
}, connectTimeoutMs);
} catch (err) {
setState(prev => ({
...prev,
error: err instanceof Error ? err : new Error(String(err)),
status: "error",
}));
}
}, [
url,
channel,
token,
connectTimeoutMs,
handleOpen,
handleMessage,
handleError,
handleClose,
setStatus,
]);
// ── lifecycle ──────────────────────────────────────────────────
useEffect(() => {
connect();
return () => {
timeoutRef.current && clearTimeout(timeoutRef.current);
heartbeatRef.current && clearInterval(heartbeatRef.current);
retryRef.current && clearTimeout(retryRef.current);
unsubscribe.current = () => wsRef.current?.close();
unsubscribe.current();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [url, channel, token]);
// ── API pública ────────────────────────────────────────────────
const send = useCallback(
(payload: unknown, _type: WSMessage["type"] = "data") => {
if (wsRef.current?.readyState !== WebSocket.OPEN) {
console.warn("[useWebSocket] Socket não está conectada — mensagem descartada");
return;
}
const envelope: WSMessage = {
id: generateId(),
channel,
type: _type,
timestamp: Date.now(),
payload,
seq: seqRef.current,
};
wsRef.current.send(JSON.stringify(envelope));
},
[channel],
);
const close = useCallback(() => {
timeoutRef.current && clearTimeout(timeoutRef.current);
heartbeatRef.current && clearInterval(heartbeatRef.current);
retryRef.current && clearTimeout(retryRef.current);
wsRef.current?.close(1000, "normal");
wsRef.current = null;
setStatus("closed");
unsubscribe.current();
}, [setStatus]);
const reconnect = useCallback(() => {
close();
setState(prev => ({ ...prev, retryCount: 0 }));
// pequena pausa antes de reconectar
setTimeout(connect, 500);
}, [close, connect]);
// ── registrar handlers externos ────────────────────────────────
useEffect(() => {
handlersRef.current = {
onMessage: config.onMessage as MessageHandler<T> | undefined,
onError: config.onError as ErrorHandler | undefined,
onStatus: config.onStatus as StatusHandler | undefined,
};
}, [config.onMessage, config.onError, config.onStatus]);
return {
...state,
send,
close,
reconnect,
} as WSState<T> & { send: (payload: unknown, type?: WSMessage["type"]) => void; close: () => void; reconnect: () => void };
}
export default useWebSocket;
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
Vendored Regular → Executable
View File
Vendored Regular → Executable
View File
Vendored Regular → Executable
View File
Vendored Regular → Executable
View File
Vendored Regular → Executable
View File
Vendored Regular → Executable
View File
View File
View File
Vendored Regular → Executable
View File
View File
Vendored Regular → Executable
View File
View File
View File
View File
Vendored Regular → Executable
View File
View File
Vendored Regular → Executable
View File
View File
View File
View File
View File
View File
View File
View File
View File
Generated Vendored Regular → Executable
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File

Some files were not shown because too many files have changed in this diff Show More