funifier-mcp 0.2.26 → 0.2.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/rules/funifier.mdc +38 -41
- package/.github/copilot-instructions.md +38 -41
- package/AGENTS.md +56 -49
- package/README.md +40 -22
- package/datasource-funifier-docs/.coverage.json +326 -0
- package/datasource-funifier-docs/.validation.json +593 -0
- package/datasource-funifier-docs/knowledge/guides/aggregates.md +182 -70
- package/datasource-funifier-docs/knowledge/guides/database-access.md +174 -88
- package/datasource-funifier-docs/knowledge/guides/java-entities.md +294 -204
- package/datasource-funifier-docs/knowledge/guides/java-libraries.md +202 -226
- package/datasource-funifier-docs/knowledge/guides/java-managers.md +343 -265
- package/datasource-funifier-docs/knowledge/guides/trigger-examples.md +180 -236
- package/datasource-funifier-docs/knowledge/guides/triggers-guide.md +273 -191
- package/datasource-funifier-docs/knowledge/index.md +4 -1
- package/datasource-funifier-docs/knowledge/modules/achievement.md +1126 -28
- package/datasource-funifier-docs/knowledge/modules/action-log.md +469 -62
- package/datasource-funifier-docs/knowledge/modules/action.md +522 -70
- package/datasource-funifier-docs/knowledge/modules/auth.md +718 -69
- package/datasource-funifier-docs/knowledge/modules/avatar.md +483 -18
- package/datasource-funifier-docs/knowledge/modules/backup.md +603 -25
- package/datasource-funifier-docs/knowledge/modules/challenge.md +1048 -220
- package/datasource-funifier-docs/knowledge/modules/compact.md +469 -26
- package/datasource-funifier-docs/knowledge/modules/competition.md +811 -109
- package/datasource-funifier-docs/knowledge/modules/crossword.md +504 -28
- package/datasource-funifier-docs/knowledge/modules/csv-data.md +645 -20
- package/datasource-funifier-docs/knowledge/modules/custom-object.md +701 -36
- package/datasource-funifier-docs/knowledge/modules/database.md +730 -164
- package/datasource-funifier-docs/knowledge/modules/folder.md +935 -280
- package/datasource-funifier-docs/knowledge/modules/kpi-formulas.md +410 -15
- package/datasource-funifier-docs/knowledge/modules/lastmile.md +568 -29
- package/datasource-funifier-docs/knowledge/modules/leaderboard.md +595 -126
- package/datasource-funifier-docs/knowledge/modules/level.md +536 -54
- package/datasource-funifier-docs/knowledge/modules/lottery.md +809 -76
- package/datasource-funifier-docs/knowledge/modules/marketplace.md +688 -17
- package/datasource-funifier-docs/knowledge/modules/mystery.md +662 -52
- package/datasource-funifier-docs/knowledge/modules/notification.md +564 -26
- package/datasource-funifier-docs/knowledge/modules/patterns.md +519 -814
- package/datasource-funifier-docs/knowledge/modules/player.md +773 -73
- package/datasource-funifier-docs/knowledge/modules/point.md +380 -83
- package/datasource-funifier-docs/knowledge/modules/public.md +508 -178
- package/datasource-funifier-docs/knowledge/modules/question.md +619 -99
- package/datasource-funifier-docs/knowledge/modules/quiz.md +565 -120
- package/datasource-funifier-docs/knowledge/modules/scheduler.md +1092 -39
- package/datasource-funifier-docs/knowledge/modules/security.md +674 -112
- package/datasource-funifier-docs/knowledge/modules/staging.md +742 -19
- package/datasource-funifier-docs/knowledge/modules/story.md +565 -29
- package/datasource-funifier-docs/knowledge/modules/studio-page.md +470 -144
- package/datasource-funifier-docs/knowledge/modules/swap.md +552 -84
- package/datasource-funifier-docs/knowledge/modules/team.md +563 -45
- package/datasource-funifier-docs/knowledge/modules/trigger.md +876 -134
- package/datasource-funifier-docs/knowledge/modules/upload.md +468 -95
- package/datasource-funifier-docs/knowledge/modules/virtual-good.md +510 -63
- package/datasource-funifier-docs/knowledge/modules/webhook.md +375 -28
- package/datasource-funifier-docs/knowledge/modules/websocket.md +459 -26
- package/datasource-funifier-docs/knowledge/modules/widget.md +613 -27
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +42 -1
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/init.test.js +74 -3
- package/dist/cli/init.test.js.map +1 -1
- package/dist/cli/persona.d.ts +3 -0
- package/dist/cli/persona.d.ts.map +1 -0
- package/dist/cli/persona.js +25 -0
- package/dist/cli/persona.js.map +1 -0
- package/dist/mcp/bundle.js +119 -93
- package/dist/mcp/check-update.d.ts +5 -0
- package/dist/mcp/check-update.d.ts.map +1 -1
- package/dist/mcp/check-update.js +21 -10
- package/dist/mcp/check-update.js.map +1 -1
- package/dist/mcp/check-update.test.d.ts +2 -0
- package/dist/mcp/check-update.test.d.ts.map +1 -0
- package/dist/mcp/check-update.test.js +33 -0
- package/dist/mcp/check-update.test.js.map +1 -0
- package/dist/mcp/index.js +2 -2
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/prompts/templates.d.ts.map +1 -1
- package/dist/mcp/prompts/templates.js +35 -0
- package/dist/mcp/prompts/templates.js.map +1 -1
- package/dist/mcp/resources/documentation.d.ts +1 -1
- package/dist/mcp/resources/documentation.d.ts.map +1 -1
- package/dist/mcp/resources/documentation.js +39 -3
- package/dist/mcp/resources/documentation.js.map +1 -1
- package/dist/mcp/tools/connect.d.ts.map +1 -1
- package/dist/mcp/tools/connect.js +18 -8
- package/dist/mcp/tools/connect.js.map +1 -1
- package/dist/mcp/tools/database.d.ts.map +1 -1
- package/dist/mcp/tools/database.js +59 -47
- package/dist/mcp/tools/database.js.map +1 -1
- package/dist/mcp/tools/database.test.js +2 -2
- package/dist/mcp/tools/database.test.js.map +1 -1
- package/dist/mcp/tools/delete.d.ts.map +1 -1
- package/dist/mcp/tools/delete.js +13 -3
- package/dist/mcp/tools/delete.js.map +1 -1
- package/dist/mcp/tools/execute.d.ts.map +1 -1
- package/dist/mcp/tools/execute.js +20 -9
- package/dist/mcp/tools/execute.js.map +1 -1
- package/dist/mcp/tools/folder.d.ts.map +1 -1
- package/dist/mcp/tools/folder.js +22 -12
- package/dist/mcp/tools/folder.js.map +1 -1
- package/dist/mcp/tools/get.d.ts.map +1 -1
- package/dist/mcp/tools/get.js +16 -6
- package/dist/mcp/tools/get.js.map +1 -1
- package/dist/mcp/tools/index.d.ts +1 -1
- package/dist/mcp/tools/index.d.ts.map +1 -1
- package/dist/mcp/tools/index.js +28 -1
- package/dist/mcp/tools/index.js.map +1 -1
- package/dist/mcp/tools/list.d.ts.map +1 -1
- package/dist/mcp/tools/list.js +38 -14
- package/dist/mcp/tools/list.js.map +1 -1
- package/dist/mcp/tools/logs.d.ts.map +1 -1
- package/dist/mcp/tools/logs.js +15 -5
- package/dist/mcp/tools/logs.js.map +1 -1
- package/dist/mcp/tools/save.d.ts.map +1 -1
- package/dist/mcp/tools/save.js +14 -4
- package/dist/mcp/tools/save.js.map +1 -1
- package/dist/mcp/tools/save.test.js +3 -3
- package/dist/mcp/tools/save.test.js.map +1 -1
- package/dist/mcp/tools/search-docs.d.ts +3 -0
- package/dist/mcp/tools/search-docs.d.ts.map +1 -0
- package/dist/mcp/tools/search-docs.js +102 -0
- package/dist/mcp/tools/search-docs.js.map +1 -0
- package/package.json +6 -2
- package/skills/acquire-funifier-knowledge/SKILL.md +155 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +86 -0
- package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
- package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
- package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
- package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
- package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
- package/skills/funifier/SKILL.md +88 -0
- package/skills/funifier/references/configure-security.md +96 -0
- package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
- package/skills/funifier/references/create-aggregate.md +144 -0
- package/skills/funifier/references/create-challenge.md +116 -0
- package/skills/funifier/references/create-competition.md +98 -0
- package/skills/funifier/references/create-crossword.md +574 -0
- package/skills/funifier/references/create-custom-object.md +91 -0
- package/skills/funifier/references/create-custom-page.md +135 -0
- package/skills/funifier/references/create-folder.md +104 -0
- package/skills/funifier/references/create-lastmile.md +643 -0
- package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
- package/skills/funifier/references/create-level.md +94 -0
- package/skills/funifier/references/create-lottery.md +913 -0
- package/skills/funifier/references/create-mystery.md +769 -0
- package/skills/funifier/references/create-notification.md +75 -0
- package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
- package/skills/funifier/references/create-quiz.md +98 -0
- package/skills/funifier/references/create-scheduler.md +141 -0
- package/skills/funifier/references/create-story.md +636 -0
- package/skills/funifier/references/create-swap.md +95 -0
- package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
- package/skills/funifier/references/create-virtual-good.md +96 -0
- package/skills/funifier/references/create-webhook.md +72 -0
- package/skills/funifier/references/create-websocket.md +71 -0
- package/skills/funifier/references/create-widget.md +76 -0
- package/skills/funifier/references/debug.md +87 -0
- package/skills/funifier/references/help.md +81 -0
- package/skills/funifier/references/implement-frontend.md +106 -0
- package/skills/funifier/references/import-csv.md +75 -0
- package/skills/funifier/references/manage-player.md +82 -0
- package/skills/funifier/references/manage-team.md +76 -0
- package/skills/funifier/references/upload-file.md +91 -0
- package/skills/funifier-create-aggregate/SKILL.md +0 -127
- package/skills/funifier-create-challenge/SKILL.md +0 -88
- package/skills/funifier-create-custom-page/SKILL.md +0 -127
- package/skills/funifier-create-level/SKILL.md +0 -87
- package/skills/funifier-create-quiz/SKILL.md +0 -87
- package/skills/funifier-create-scheduler/SKILL.md +0 -127
- package/skills/funifier-create-virtual-good/SKILL.md +0 -87
- package/skills/funifier-debug/SKILL.md +0 -92
- package/skills/funifier-help/SKILL.md +0 -86
- package/skills/funifier-implement-frontend/SKILL.md +0 -90
- package/skills/funifier-index/SKILL.md +0 -58
|
@@ -1,41 +1,474 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `websocket`
|
|
2
2
|
|
|
3
3
|
**Acesso Studio:** `/studio/websocket`
|
|
4
4
|
**API Endpoint:** `/v3/websocket`
|
|
5
|
+
**Endpoint WS (runtime):** `ws://<host>/ws/{socket}`
|
|
6
|
+
**Coleção MongoDB:** `websocket`
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
---
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
## 1. Visão Geral
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
O módulo `websocket` é um **motor de execução de scripts Groovy acionados por eventos de conexão WebSocket**. Cada documento `websocket` guarda um script Groovy que é compilado, cacheado em memória e executado quando um cliente abre, envia mensagem ou fecha uma conexão WebSocket cujo path aponta para o `_id` daquele documento.
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
- Para feedback instantâneo ao completar desafios
|
|
14
|
-
- Para chats ao vivo entre jogadores
|
|
15
|
-
- Para atualização de status em tempo real
|
|
14
|
+
É um irmão mais simples do módulo [`trigger`](trigger.md): compartilha exatamente a mesma infraestrutura de sandbox (`SecureASTCustomizer` + `TriggerExpressionChecker`), o mesmo padrão de cache de classe compilada invalidado por `updated`, e o mesmo executor isolado com timeout. A diferença é o gatilho: em vez de eventos de ciclo de vida de entidades, o gatilho são os eventos de socket `onOpen` / `onMessage` / `onClose`.
|
|
16
15
|
|
|
17
|
-
|
|
16
|
+
Papel arquitetural:
|
|
18
17
|
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
18
|
+
- Permite implementar lógica de comunicação em tempo real (chat, broadcast, notificação push para o navegador) sem recompilar o serviço.
|
|
19
|
+
- O endpoint físico WebSocket (`WebSocketServer`, JSR-356 `@ServerEndpoint("/ws/{socket}")`) recebe a conexão, resolve o tenant pela apiKey extraída do token e delega cada evento ao `WebSocketManager`.
|
|
20
|
+
- O `WebSocketManager` mantém **em memória** o mapa de sessões abertas por socket e o cache de classes Groovy compiladas.
|
|
21
|
+
- O envio efetivo de bytes ao navegador só acontece **de dentro do próprio script**, via o helper `broadcast(...).send()` (classe `Message`).
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Relação com outros módulos:
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
- **`trigger`** — reutiliza `TriggerExpressionChecker` (mesma lista de classes/métodos proibidos no AST).
|
|
26
|
+
- **`ai`** — `POST /v3/ai/build/websocket` (`AIRest.buildWebSocket`) gera um rascunho de `WebSocket` via GPT-3.5. Ver seção 4 e seção 7 (contém bugs de copy-paste).
|
|
27
|
+
- **`auth`** — autenticação da conexão WS via token na querystring (`AuthBean`, `TokenUtil`).
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
**Método:** POST
|
|
31
|
-
**Endpoint:** `/v3/websocket`
|
|
29
|
+
> ⚠️ **Aviso de runtime, confirmado no código:** o registro programático do endpoint WebSocket está **comentado** em `Configuration.java:544-553`. Ver seção 7 e seção 8 — esta é a informação operacional mais importante deste módulo.
|
|
32
30
|
|
|
33
|
-
|
|
34
|
-
**Método:** DELETE
|
|
35
|
-
**Endpoint:** `/v3/websocket/:id`
|
|
31
|
+
---
|
|
36
32
|
|
|
37
|
-
##
|
|
33
|
+
## 2. Arquitetura e Fluxos
|
|
38
34
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
### 2.1 Classes envolvidas
|
|
36
|
+
|
|
37
|
+
| Classe | Arquivo | Papel |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| `WebSocket` | `engine/websocket/WebSocket.java` | Entidade persistida — script + metadados. |
|
|
40
|
+
| `WebSocketManager` | `engine/websocket/WebSocketManager.java` | Orquestrador — CRUD, mapa de sessões, cache `compiled`/`dates`, compilação e execução. |
|
|
41
|
+
| `Message` | `engine/websocket/Message.java` | Helper de broadcast — filtra sessões por player e envia texto. |
|
|
42
|
+
| `WebSocketServer` | `ws/WebSocketServer.java` | Endpoint físico JSR-356 `@ServerEndpoint("/ws/{socket}")`. |
|
|
43
|
+
| `WebSocketRest` | `rest/v3/rest/WebSocketRest.java` | Endpoints REST `/v3/websocket` (CRUD da definição). |
|
|
44
|
+
| `TriggerExpressionChecker` | `engine/integration/trigger/TriggerExpressionChecker.java` | `SecureASTCustomizer.ExpressionChecker` reaproveitado do `trigger`. |
|
|
45
|
+
| `ManagerFactory` | `controller/ManagerFactory.java` | Instancia **um** `WebSocketManager` por tenant (`getWebSocketManager()`). |
|
|
46
|
+
| `Entity.WEBSOCKET` | `engine/util/Entity.java:242` | Mapeia a coleção `websocket` → `WebSocket.class`. |
|
|
47
|
+
|
|
48
|
+
### 2.2 Pipeline de cadastro (REST `POST /v3/websocket`)
|
|
49
|
+
|
|
50
|
+
Sequência real (`WebSocketRest.insert`):
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
1. [Entrada] insert(authBean, obj) — obj == null → retorna {} com HTTP 201 e nada acontece
|
|
54
|
+
2. [Resolve] getManagerFactory().getWebSocketManager()
|
|
55
|
+
3. [Monta script] manager.getScript(obj) — embrulha obj.script na classe FunifierWebSocket
|
|
56
|
+
4. [Valida] manager.compile(script) — compila com SecureASTCustomizer
|
|
57
|
+
├─ CompilationFailedException → result = {websocket, status:"ERROR", message} → HTTP 201
|
|
58
|
+
└─ OK →
|
|
59
|
+
5. [Persiste] manager.save(obj) — upsert por _id na coleção `websocket`
|
|
60
|
+
6. [Resposta] result = {websocket, status:"OK"} → HTTP 201
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Pontos não-convencionais (ver seção 4):
|
|
64
|
+
- O HTTP **201 é retornado mesmo quando a compilação falha** — o campo `status` no corpo é o sinal real de sucesso/erro.
|
|
65
|
+
- `compile()` é chamado **antes** de `save()`: um script inválido nunca é persistido.
|
|
66
|
+
|
|
67
|
+
### 2.3 Pipeline de execução de evento (runtime WS)
|
|
68
|
+
|
|
69
|
+
Disparado por `WebSocketServer.onOpen/onMessage/onClose` → `WebSocketManager.onOpen/onMessage/onClose` → `run(...)` → `executor(...)`.
|
|
70
|
+
|
|
71
|
+
Pseudocódigo de `WebSocketManager.run`:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
se websocket.updated == null:
|
|
75
|
+
save(websocket) # força criação e define updated
|
|
76
|
+
lastCompilation = dates[websocket._id]
|
|
77
|
+
clazz = compiled[websocket._id]
|
|
78
|
+
se clazz != null E lastCompilation != null E lastCompilation > websocket.updated:
|
|
79
|
+
executor(clazz, ...) # reaproveita classe em cache
|
|
80
|
+
senão:
|
|
81
|
+
script = getScript(websocket)
|
|
82
|
+
clazz = compile(script)
|
|
83
|
+
compiled[websocket._id] = clazz
|
|
84
|
+
dates[websocket._id] = agora # marca data da compilação
|
|
85
|
+
executor(clazz, ...)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Fluxo de compilação e cache — `WebSocketManager.run`
|
|
89
|
+
|
|
90
|
+
```mermaid
|
|
91
|
+
flowchart TD
|
|
92
|
+
A[Evento de socket] --> B{websocket.updated == null?}
|
|
93
|
+
B -- sim --> C[save: define updated]
|
|
94
|
+
B -- nao --> D{cache valido?<br/>lastCompilation > updated}
|
|
95
|
+
C --> D
|
|
96
|
+
D -- sim --> E[reutiliza classe em compiled]
|
|
97
|
+
D -- nao --> F[getScript + compile]
|
|
98
|
+
F --> G[guarda em compiled e dates]
|
|
99
|
+
G --> H[executor]
|
|
100
|
+
E --> H[executor: thread isolada + timeout]
|
|
101
|
+
H --> I{metodo}
|
|
102
|
+
I -- onOpen --> J[script.onOpen session, config]
|
|
103
|
+
I -- onMessage --> K[script.onMessage session, message]
|
|
104
|
+
I -- onClose --> L[script.onClose session, reason]
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`WebSocketManager.executor` (síncrono, bloqueante por evento):
|
|
108
|
+
|
|
109
|
+
1. Cria `Executors.newSingleThreadExecutor()`.
|
|
110
|
+
2. `timeout = websocket.timeout` se `> 0`, senão `30` segundos.
|
|
111
|
+
3. Instancia a classe compilada (`clazz.newInstance()`).
|
|
112
|
+
4. Injeta `setId`, `setContext`, `setManager`, `setSessions` no objeto Groovy.
|
|
113
|
+
5. Despacha **apenas** `onOpen` | `onMessage` | `onClose` conforme o evento (⚠️ `onError` não é despachado — seção 7).
|
|
114
|
+
6. Chama `addAllOutput` para coletar os `println` do script.
|
|
115
|
+
7. `future.get(timeout, SECONDS)` — se estourar, `future.cancel(true)` + `executor.shutdownNow()`.
|
|
116
|
+
8. Qualquer exceção (compilação, segurança, timeout, execução) é coletada na lista `exceptions` — que é **descartada** pelos chamadores `onOpen/onMessage/onClose` (não há retorno ao cliente). Falhas de script são, na prática, silenciosas em runtime (no máximo um `printStackTrace`).
|
|
117
|
+
|
|
118
|
+
### 2.4 Interação entre componentes (push para o navegador)
|
|
119
|
+
|
|
120
|
+
```mermaid
|
|
121
|
+
sequenceDiagram
|
|
122
|
+
participant B as Browser
|
|
123
|
+
participant S as WebSocketServer<br/>(/ws/{socket})
|
|
124
|
+
participant M as WebSocketManager
|
|
125
|
+
participant G as Script Groovy<br/>(FunifierWebSocket)
|
|
126
|
+
participant Msg as Message
|
|
127
|
+
B->>S: WS connect ?authorization=Bearer&player=alice
|
|
128
|
+
S->>S: getApiKey(token) / getPlayer(token|?player)
|
|
129
|
+
S->>S: session.userProperties["id"] = player
|
|
130
|
+
S->>M: onOpen(socket, session, config)
|
|
131
|
+
M->>M: getSessions(socket).add(session) (via script)
|
|
132
|
+
B->>S: send("texto")
|
|
133
|
+
S->>M: onMessage(socket, session, "texto")
|
|
134
|
+
M->>G: onMessage(session, message)
|
|
135
|
+
G->>Msg: broadcast(message).to("alice").send()
|
|
136
|
+
Msg->>M: getSessions(socket)
|
|
137
|
+
Msg-->>B: session.getAsyncRemote().sendText(message)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
> O `WebSocketManager.send(socket, message)` **não** envia diretamente — ele delega para `onMessage(socket, null, message)`, ou seja, re-executa o script do socket com `session = null`. O laço de broadcast direto está comentado no código. O único caminho que efetivamente escreve bytes no cliente é `Message.send()`, invocado de dentro do script via `broadcast(...)`.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## 3. Estrutura dos Objetos
|
|
145
|
+
|
|
146
|
+
### 3.1 `WebSocket` — documento raiz
|
|
147
|
+
|
|
148
|
+
Coleção `websocket`. Anotada com `@JsonIgnoreProperties(ignoreUnknown=true)` → **qualquer campo não listado abaixo é silenciosamente descartado na desserialização**.
|
|
149
|
+
|
|
150
|
+
| Campo | Tipo | Padrão | Obrigatório | Descrição |
|
|
151
|
+
|---|---|---|---|---|
|
|
152
|
+
| `_id` | String | auto (`Guid.shortTimeMillis()` se vazio/branco) | — | Identificador. **É usado como `{socket}` na URL** `ws://<host>/ws/{_id}`. Defina um valor legível (ex. `reward`, `chat`). |
|
|
153
|
+
| `title` | String | — | não | Nome de exibição no Studio. **Não usado em runtime.** |
|
|
154
|
+
| `description` | String | — | não | Descrição. **Não usado em runtime.** |
|
|
155
|
+
| `script` | String | — | sim (de fato) | Código Groovy com os métodos `onOpen` / `onMessage` / `onClose`. Sem ele, a compilação gera uma classe vazia e nenhum evento faz nada. |
|
|
156
|
+
| `timeout` | Long | 30 (fallback no `executor`) | não | Timeout em **segundos** do executor por evento. Ver dupla-camada de timeout abaixo. |
|
|
157
|
+
| `updated` | Date | auto (`new Date()` a cada `save`) | — | Timestamp da última gravação **e** invalidador de cache cross-cluster (ver 3.2). |
|
|
158
|
+
|
|
159
|
+
#### Campos aceitos e silenciosamente ignorados
|
|
160
|
+
|
|
161
|
+
- `aggregate`, `collection` — **não existem** na classe `WebSocket`. São sugeridos pelo prompt do builder de IA (`AIRest.buildWebSocket`) por copy-paste do builder de *find/aggregate*, e descartados por `@JsonIgnoreProperties` ao desserializar a resposta do GPT. Ver seção 7.
|
|
162
|
+
- Qualquer outro campo enviado no POST é descartado sem erro.
|
|
163
|
+
|
|
164
|
+
#### Campos sem persistência (apenas runtime, injetados no script)
|
|
165
|
+
|
|
166
|
+
Não ficam no documento — são variáveis disponíveis **dentro** do script Groovy (ver 3.3): `_id`, `sessions`, `context`, `manager`, `output`.
|
|
167
|
+
|
|
168
|
+
### 3.2 Semântica de `updated` (cache cross-cluster)
|
|
169
|
+
|
|
170
|
+
`updated` tem papel duplo:
|
|
171
|
+
|
|
172
|
+
1. Timestamp de auditoria da última gravação.
|
|
173
|
+
2. **Invalidador de cache distribuído.** Cada nó do cluster mantém seu próprio `dates[_id]` (data da última compilação local). A condição de reuso de cache é `lastCompilation > websocket.updated`. Quando o nó A salva o script (`save()` atualiza `updated`), o nó B — ao reler o documento do Mongo no próximo evento — verá `updated > lastCompilation` local e **recompilará**, propagando a nova versão sem reinício. Os comentários `//cluster:` no código confirmam esse mecanismo.
|
|
174
|
+
|
|
175
|
+
### 3.3 Classe gerada `FunifierWebSocket` (runtime)
|
|
176
|
+
|
|
177
|
+
`WebSocketManager.getScript()` embrulha o `script` do usuário numa classe Groovy anotada com `@TimedInterrupt(value = 30L, unit = TimeUnit.SECONDS)` e com um amplo conjunto de imports (HTTP/Unirest, e-mail, Thumbnailator, entidades Funifier, `ManagerFactory`, classes `javax.websocket.*`, `Message`). Helpers injetados disponíveis para o script:
|
|
178
|
+
|
|
179
|
+
| Símbolo | Tipo | Significado |
|
|
180
|
+
|---|---|---|
|
|
181
|
+
| `_id` | String | Id do socket atual. |
|
|
182
|
+
| `broadcast(String message)` | `Message` | Cria um `Message` ligado a este socket — encadeie `.to(...)` / `.tos(...)` e finalize com `.send()`. |
|
|
183
|
+
| `sessions` | `Set<Session>` | Sessões abertas deste socket (em memória, neste nó). |
|
|
184
|
+
| `context` | `HashMap` | Mapa **efêmero por evento** — alocado novo a cada `onOpen`/`onMessage`/`onClose`. **Não persiste entre eventos.** |
|
|
185
|
+
| `manager` | `ManagerFactory` | Acesso a todos os managers Funifier (player, action, etc.). |
|
|
186
|
+
| `println(msg)` | void (override) | Não imprime no stdout — acumula em `output` (coletado em `addAllOutput`, mas descartado pelos chamadores). |
|
|
187
|
+
|
|
188
|
+
### 3.4 `Message` — helper de broadcast
|
|
189
|
+
|
|
190
|
+
| Método | Efeito |
|
|
191
|
+
|---|---|
|
|
192
|
+
| `to(String player)` | Adiciona um player à lista de destinatários (ignora null/branco). |
|
|
193
|
+
| `tos(List<String> players)` | Adiciona vários players. |
|
|
194
|
+
| `send()` | Se a lista `to` estiver preenchida, envia **apenas** às sessões cujo `session.getUserProperties().get("id")` casa com um player da lista; caso contrário, faz **broadcast a todas** as sessões do socket. Envio via `getAsyncRemote().sendText(message)`. |
|
|
195
|
+
|
|
196
|
+
> O filtro por player só funciona se o cliente conectou com `player=<id>` na querystring **ou** com um token cujo claim `player` seja `<id>` (definido em `WebSocketServer.onOpen`).
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## 4. Endpoints
|
|
201
|
+
|
|
202
|
+
### `POST /v3/websocket` — criar ou substituir (upsert)
|
|
203
|
+
|
|
204
|
+
| Aspecto | Detalhe |
|
|
205
|
+
|---|---|
|
|
206
|
+
| Finalidade | Compila e persiste a definição do websocket. |
|
|
207
|
+
| Autenticação | `@BeanParam AuthBean` (Basic/Bearer). |
|
|
208
|
+
| Full replace ou patch | **Full replace / upsert por `_id`** (`col.save(obj)` do Jongo). Não há merge de campos. |
|
|
209
|
+
| Código HTTP | Sempre `201 CREATED`, inclusive em erro de compilação. |
|
|
210
|
+
|
|
211
|
+
**Comportamento real:**
|
|
212
|
+
- `obj == null` → resposta `{}` (corpo vazio após remoção de nulls) com HTTP 201; nada é persistido.
|
|
213
|
+
- Compila **antes** de salvar. Em `CompilationFailedException`, retorna `status:"ERROR"` + `message` e **não salva**.
|
|
214
|
+
- `_id` ausente/branco → gerado por `Guid.shortTimeMillis()` (string base-encoded de `System.currentTimeMillis()`).
|
|
215
|
+
- `updated` é sempre sobrescrito com `new Date()`.
|
|
216
|
+
|
|
217
|
+
**Request:**
|
|
218
|
+
|
|
219
|
+
```json
|
|
220
|
+
{
|
|
221
|
+
"_id": "chat",
|
|
222
|
+
"title": "Chat ao vivo",
|
|
223
|
+
"description": "Canal de chat da competição",
|
|
224
|
+
"timeout": 30,
|
|
225
|
+
"script": "void onMessage(session, message) {\n broadcast(message).send();\n}"
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
**Response (sucesso):**
|
|
230
|
+
|
|
231
|
+
```json
|
|
232
|
+
{
|
|
233
|
+
"websocket": {
|
|
234
|
+
"_id": "chat",
|
|
235
|
+
"title": "Chat ao vivo",
|
|
236
|
+
"description": "Canal de chat da competição",
|
|
237
|
+
"timeout": 30,
|
|
238
|
+
"script": "void onMessage(session, message) { broadcast(message).send(); }",
|
|
239
|
+
"updated": { "$date": "2026-05-20T12:00:00.000Z" }
|
|
240
|
+
},
|
|
241
|
+
"status": "OK"
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**Response (erro de compilação) — ainda HTTP 201:**
|
|
246
|
+
|
|
247
|
+
```json
|
|
248
|
+
{
|
|
249
|
+
"websocket": { "_id": "chat", "script": "void onMessage(... }" },
|
|
250
|
+
"status": "ERROR",
|
|
251
|
+
"message": "startup failed: Script1.groovy: 1: unexpected token ..."
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### `GET /v3/websocket/{id}` — buscar uma definição
|
|
256
|
+
|
|
257
|
+
| Aspecto | Detalhe |
|
|
258
|
+
|---|---|
|
|
259
|
+
| Finalidade | Retorna o documento `WebSocket` por `_id`. |
|
|
260
|
+
| Autenticação | `@BeanParam AuthBean`. |
|
|
261
|
+
| Não encontrado | `find()` faz `findOne("{_id:#}", id)`; se não existir, o corpo serializado é vazio/nulo, HTTP 200. |
|
|
262
|
+
|
|
263
|
+
```
|
|
264
|
+
GET /v3/websocket/chat
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### `DELETE /v3/websocket/{id}` — remover
|
|
268
|
+
|
|
269
|
+
| Aspecto | Detalhe |
|
|
270
|
+
|---|---|
|
|
271
|
+
| Finalidade | Remove o documento e limpa o estado em memória. |
|
|
272
|
+
| Autenticação | `@BeanParam AuthBean`. |
|
|
273
|
+
| Código HTTP | `204 NO_CONTENT`. |
|
|
274
|
+
|
|
275
|
+
Efeitos: `remove("{_id:#}", id)` na coleção, `compiled.remove(id)`, `dates.remove(id)`, `sessions.remove(id)` (descarta o mapa de sessões em memória deste nó).
|
|
276
|
+
|
|
277
|
+
### `POST /v3/ai/build/websocket` — rascunho via IA (não persiste)
|
|
278
|
+
|
|
279
|
+
| Aspecto | Detalhe |
|
|
280
|
+
|---|---|
|
|
281
|
+
| Finalidade | Usa GPT-3.5 (function calling `build_websocket`) para gerar um rascunho de `WebSocket` a partir de uma descrição em linguagem natural. |
|
|
282
|
+
| Autenticação | `@BeanParam AuthBean`. |
|
|
283
|
+
| Persistência | **Nenhuma.** Retorna um objeto `WebSocket` (HTTP 200) que o cliente deve então enviar ao `POST /v3/websocket`. |
|
|
284
|
+
|
|
285
|
+
**Request:** `{ "content": "um chat onde a mensagem vai só para o player citado" }`
|
|
286
|
+
|
|
287
|
+
> ⚠️ Contém dois bugs de copy-paste confirmados no código — ver seção 7.
|
|
288
|
+
|
|
289
|
+
### Endpoint WebSocket (runtime) — `ws://<host>/ws/{socket}`
|
|
290
|
+
|
|
291
|
+
JSR-356 `@ServerEndpoint("/ws/{socket}")` em `WebSocketServer`. `{socket}` = `_id` do documento `websocket`.
|
|
292
|
+
|
|
293
|
+
**Query params da conexão:**
|
|
294
|
+
|
|
295
|
+
| Param | Tipo | Descrição |
|
|
296
|
+
|---|---|---|
|
|
297
|
+
| `authorization` | String | `Bearer <token>`. Usado para extrair a apiKey (`AuthBean.getApiKey`) → resolve o tenant/banco, e o claim `player` (`TokenUtil.getValue(..., "player")`). |
|
|
298
|
+
| `player` | String | Fallback do id do player, usado **apenas** se o token não trouxer o claim `player`. |
|
|
299
|
+
|
|
300
|
+
No `onOpen`, se um player for resolvido, `session.getUserProperties().put("id", player)` — é esse valor que o `Message.to(...)` usa para filtrar destinatários.
|
|
301
|
+
|
|
302
|
+
```
|
|
303
|
+
ws://service.funifier.com/ws/chat?authorization=Bearer%20<JWT>&player=alice
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## 5. Regras de Negócio
|
|
309
|
+
|
|
310
|
+
Regras presentes no código (não no schema):
|
|
311
|
+
|
|
312
|
+
- **Compilação obrigatória no cadastro** — script inválido nunca chega ao banco (mas a resposta ainda é 201; cheque `status`).
|
|
313
|
+
- **Isolamento por tenant via apiKey** — não há campo de organização no documento. O isolamento vem do roteamento `FrontController.getInstance(apiKey).getManagerFactory()`: cada apiKey resolve uma conexão/banco Mongo distinto. O `find(socket)` ocorre no banco do tenant dono do token usado na conexão WS.
|
|
314
|
+
- **Dupla camada de timeout** — (1) `@TimedInterrupt(value = 30L, SECONDS)` é injetado **hardcoded** na classe gerada por `getScript()` e instrumenta laços/iterações em nível de AST; (2) `websocket.timeout` (ou 30s) limita o `future.get(...)` do executor. Definir `timeout > 30` **não** contorna o `@TimedInterrupt` de 30s para laços apertados.
|
|
315
|
+
- **Cache invalidado por gravação** — salvar uma definição (`save()`) atualiza `updated` e remove a classe do `compiled` deste nó; demais nós recompilam quando detectam `updated > lastCompilation` (ver 3.2).
|
|
316
|
+
- **Sessões em memória** — o mapa `sessions` é um `HashMap` simples no `WebSocketManager`, vivo apenas no processo. Ver limitação de cluster na seção 7/8.
|
|
317
|
+
- **Execução síncrona e bloqueante por evento** — cada evento roda em uma thread dedicada e o `WebSocketServer` aguarda o resultado.
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## 6. Comportamentos Automáticos
|
|
322
|
+
|
|
323
|
+
| Comportamento | Trigger | Impacto | Persistência |
|
|
324
|
+
|---|---|---|---|
|
|
325
|
+
| Geração de `_id` | `save()` com `_id` vazio/branco | `Guid.shortTimeMillis()` define o id (que vira a URL do socket) | Sim (`websocket`) |
|
|
326
|
+
| Atualização de `updated` | Todo `save()` | Marca timestamp e invalida cache cross-cluster | Sim |
|
|
327
|
+
| Invalidação de cache local | `save()` e `delete()` | `compiled.remove(_id)` (e `dates`/`sessions` no delete) | Memória (por nó) |
|
|
328
|
+
| Auto-criação no 1º evento | `run()` com `websocket.updated == null` | Chama `save()` antes de executar — persiste a definição que ainda não tinha sido gravada | Sim |
|
|
329
|
+
| Recompilação automática | `run()` quando cache inválido | Recompila o script e atualiza `dates[_id]` | Memória |
|
|
330
|
+
| `context` efêmero | Cada `onOpen`/`onMessage`/`onClose` | `HashMap` novo por evento; não há estado entre eventos | Não |
|
|
331
|
+
| Coleta de `println` | `executor` → `addAllOutput` | Saídas do script acumuladas em lista e **descartadas** pelos chamadores | Não |
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## 7. Suportado vs NÃO Suportado
|
|
336
|
+
|
|
337
|
+
### ✅ Suportado
|
|
338
|
+
|
|
339
|
+
- CRUD da definição: `POST /v3/websocket` (upsert), `GET /v3/websocket/{id}`, `DELETE /v3/websocket/{id}`.
|
|
340
|
+
- Eventos de script: `onOpen(session, config)`, `onMessage(session, message)`, `onClose(session, reason)`.
|
|
341
|
+
- Broadcast a todas as sessões do socket (`broadcast(msg).send()`).
|
|
342
|
+
- Envio direcionado por player (`broadcast(msg).to("alice").send()` / `.tos([...])`).
|
|
343
|
+
- Acesso aos managers Funifier dentro do script (`manager.getPlayerManager()`, etc.).
|
|
344
|
+
- Geração de rascunho por IA (`POST /v3/ai/build/websocket`).
|
|
345
|
+
- Sandbox de segurança herdado do `trigger` (ver seção 8).
|
|
346
|
+
- Pong automático em mensagens `PongMessage` (`WebSocketServer.onPong`).
|
|
347
|
+
|
|
348
|
+
### ❌ NÃO Suportado
|
|
349
|
+
|
|
350
|
+
- **`GET /v3/websocket` (listagem)** — **não existe**. Não há endpoint de lista; só busca por `{id}`. (A documentação anterior afirmava o contrário — estava incorreta.)
|
|
351
|
+
- **PATCH / update parcial** — `POST` é sempre full replace/upsert; não há merge de campos.
|
|
352
|
+
- **Evento `onError` no script** — `executor` despacha **apenas** `onOpen`/`onMessage`/`onClose`. O `WebSocketServer.onError` só faz `printStackTrace()`. O comentário em `WebSocket.java:19` ("onOpen, onMessage, onClose, orError") é enganoso: um método `onError` no script **nunca** é chamado.
|
|
353
|
+
- **Campos `aggregate` e `collection`** — sugeridos pelo prompt do builder de IA, mas **inexistentes** na entidade; descartados silenciosamente por `@JsonIgnoreProperties`.
|
|
354
|
+
- **Broadcast cross-cluster** — `sessions` é um `HashMap` em memória por JVM. Um `broadcast` no nó A **não alcança** sessões conectadas ao nó B. Limitação arquitetural confirmada (não há backplane/pub-sub).
|
|
355
|
+
- **Push a partir de outros módulos** — nenhum manager fora de `engine/websocket` constrói `Message` ou chama `getWebSocketManager().send/onMessage` (verificado por grep no `src/main/java`). O único caminho de push é de dentro do próprio script do socket.
|
|
356
|
+
- **Estado persistente entre eventos** — `context` é recriado a cada evento; não há sessão de estado server-side além do `Set<Session>`.
|
|
357
|
+
- **Helpers `socket()` / `message()` no `WebSocketManager`** — comentados (deprecated).
|
|
358
|
+
- **Registro programático do endpoint WS** — `serverContainer.addEndpoint(WebSocketServer.class)` está **comentado** em `Configuration.java:544-553`. O endpoint `/ws/{socket}` depende inteiramente do auto-scan de anotações do container servlet; se o container não escanear `@ServerEndpoint`, o WebSocket **não sobe**. Confirme a ativação no ambiente antes de assumir que o módulo está operacional.
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## 8. Segurança e Permissões
|
|
363
|
+
|
|
364
|
+
- **Autenticação REST** — endpoints `/v3/websocket` usam `@BeanParam AuthBean` (Basic/Bearer), como o restante da API v3.
|
|
365
|
+
- **Autenticação WS** — feita pela querystring: `authorization=Bearer <token>` → `AuthBean.getApiKey` resolve o tenant; o claim `player` (ou `?player=`) identifica o usuário. Não há challenge handshake adicional — quem tiver o token entra.
|
|
366
|
+
- **Isolamento multi-tenant** — garantido pelo roteamento por apiKey no `FrontController` (banco Mongo distinto por tenant). O documento `websocket` não carrega campo de organização.
|
|
367
|
+
- **Sandbox de execução (Groovy)** — `compile()` usa `SecureASTCustomizer` + `TriggerExpressionChecker` (compartilhado com `trigger`):
|
|
368
|
+
- **Tipos bloqueados:** `System`, `ProcessBuilder`, `File`, `GroovyShell`, `GroovyObject`.
|
|
369
|
+
- **Textos de expressão bloqueados:** `.execute`, `.getDB`, `.getMongo`, `.dropDatabase`.
|
|
370
|
+
- **Whitelist de tokens** aritméticos/lógicos/comparação e colchetes (ver `compile()`).
|
|
371
|
+
- Limite de tempo via `@TimedInterrupt` (30s, hardcoded) + timeout do executor.
|
|
372
|
+
- **Superfícies de risco confirmadas no código:**
|
|
373
|
+
- O sandbox é **parcial** (igual ao `trigger`): a lista de classes proibidas é curta e baseada em correspondência textual da expressão (`indexOf`), contornável por reflexão indireta/aliases. Não trate scripts de fontes não confiáveis como seguros.
|
|
374
|
+
- O script tem acesso ao `ManagerFactory` completo do tenant — pode ler/alterar qualquer entidade da gamificação daquele tenant.
|
|
375
|
+
- A apiKey e o player são **logados em stdout** (`System.out.println`) em cada evento (`WebSocketServer`). Evite logs em ambiente compartilhado / cuidado com vazamento.
|
|
376
|
+
- Falhas de script são silenciosas para o cliente (exceções coletadas e descartadas) — dificulta detecção de abuso/erro.
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
## 9. Observabilidade e Troubleshooting
|
|
381
|
+
|
|
382
|
+
### Como verificar se o módulo está funcionando
|
|
383
|
+
|
|
384
|
+
1. **A definição existe?**
|
|
385
|
+
```
|
|
386
|
+
GET /v3/websocket/<socket>
|
|
387
|
+
```
|
|
388
|
+
Corpo vazio = documento inexistente (crie via POST).
|
|
389
|
+
|
|
390
|
+
2. **O script compila?** Reenvie via `POST /v3/websocket` e cheque `status`:
|
|
391
|
+
- `status: "OK"` → compilou e foi salvo.
|
|
392
|
+
- `status: "ERROR"` → veja `message` (erro de compilação Groovy). **Lembre:** HTTP é 201 nos dois casos.
|
|
393
|
+
|
|
394
|
+
3. **O endpoint WS está ativo?** Tente conectar `ws://<host>/ws/<socket>?authorization=Bearer <token>`. Se a conexão não estabelece, verifique primeiro se o `@ServerEndpoint` foi registrado pelo container — o registro programático está comentado (`Configuration.java`).
|
|
395
|
+
|
|
396
|
+
### Queries úteis (Mongo / coleção `websocket`)
|
|
397
|
+
|
|
398
|
+
```
|
|
399
|
+
// listar todos os sockets do tenant (não há endpoint REST de lista)
|
|
400
|
+
db.websocket.find({}, {title:1, updated:1})
|
|
401
|
+
|
|
402
|
+
// inspecionar um script
|
|
403
|
+
db.websocket.findOne({_id: "chat"})
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### Erros comuns e causas
|
|
407
|
+
|
|
408
|
+
| Sintoma | Causa provável |
|
|
409
|
+
|---|---|
|
|
410
|
+
| Conexão WS recusada / 404 | Endpoint não registrado (registro comentado em `Configuration.java`); container não fez auto-scan. |
|
|
411
|
+
| `status: "ERROR"` no POST | Sintaxe Groovy inválida ou uso de classe/expressão bloqueada pelo sandbox. |
|
|
412
|
+
| Mensagem não chega ao destinatário | Player do destinatário não bateu (`to("x")` exige conexão com `player=x` ou claim `player=x`). |
|
|
413
|
+
| Mensagem não chega em ambiente com vários nós | Destinatário conectado a outro nó do cluster (sessões são por JVM). |
|
|
414
|
+
| Script "trava" e nada acontece | Timeout do executor/`@TimedInterrupt` (30s) abateu a execução; exceção foi descartada silenciosamente. |
|
|
415
|
+
| `onError` no script nunca executa | `onError` não é despachado — comportamento esperado (seção 7). |
|
|
416
|
+
| Campos `aggregate`/`collection` "somem" | Descartados por `@JsonIgnoreProperties` — não existem na entidade. |
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
## 10. Exemplos Práticos
|
|
421
|
+
|
|
422
|
+
### 10.1 Mínimo funcional — eco/broadcast
|
|
423
|
+
|
|
424
|
+
```json
|
|
425
|
+
{
|
|
426
|
+
"_id": "echo",
|
|
427
|
+
"title": "Echo",
|
|
428
|
+
"script": "void onMessage(session, message) {\n broadcast(message).send();\n}"
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
Toda mensagem recebida é reenviada a todas as sessões conectadas em `/ws/echo`.
|
|
433
|
+
|
|
434
|
+
### 10.2 Avançado — mensagem direcionada por player + uso de manager
|
|
435
|
+
|
|
436
|
+
```json
|
|
437
|
+
{
|
|
438
|
+
"_id": "reward",
|
|
439
|
+
"title": "Notificação de recompensa",
|
|
440
|
+
"description": "Avisa um player específico quando algo acontece",
|
|
441
|
+
"timeout": 30,
|
|
442
|
+
"script": "void onMessage(session, message) {\n def msg = JsonUtil.fromJson(message, HashMap.class);\n String player = msg.get(\"player\");\n Player p = manager.getPlayerManager().findById(player);\n String payload = JsonUtil.toJson([player: player, name: p?.name, text: msg.get(\"text\")]);\n broadcast(payload).to(player).send();\n}"
|
|
443
|
+
}
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
A mensagem só chega à(s) sessão(ões) cujo `player` casa com o id informado (exige conexão com `?player=<id>` ou claim `player`).
|
|
447
|
+
|
|
448
|
+
### 10.3 Anti-pattern — o que NÃO fazer
|
|
449
|
+
|
|
450
|
+
```json
|
|
451
|
+
{
|
|
452
|
+
"_id": "ruim",
|
|
453
|
+
"script": "void onOpen(session, config) {\n context.put(\"count\", 0);\n}\nvoid onMessage(session, message) {\n context.count = context.count + 1; // NUNCA persiste entre eventos\n broadcast(\"total: \" + context.count).send();\n}\nvoid onError(session, t) {\n broadcast(\"erro\").send(); // NUNCA é chamado\n}"
|
|
454
|
+
}
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
Por que é errado:
|
|
458
|
+
- `context` é recriado a cada evento — `count` sempre será `null`/0 no `onMessage`. Para contadores, use o `manager`/banco.
|
|
459
|
+
- `onError` **não é despachado** — esse método é código morto.
|
|
460
|
+
- Confiar em broadcast global em produção multi-nó: só atinge sessões do nó local.
|
|
461
|
+
|
|
462
|
+
---
|
|
463
|
+
|
|
464
|
+
## Checklist de Configuração
|
|
465
|
+
|
|
466
|
+
- [ ] `_id` definido com valor legível e estável (vira a URL `/ws/{_id}`) — se omitido, vira um hash gerado.
|
|
467
|
+
- [ ] `script` contém ao menos um de `onOpen` / `onMessage` / `onClose` (não use `onError` — não é despachado).
|
|
468
|
+
- [ ] POST retornou `status: "OK"` (não confie só no HTTP 201).
|
|
469
|
+
- [ ] Para envio direcionado, confirme que os clientes conectam com `player=<id>` ou token com claim `player`.
|
|
470
|
+
- [ ] `timeout` ≤ 30s é efetivo; valores maiores não contornam o `@TimedInterrupt` de 30s para laços.
|
|
471
|
+
- [ ] **Armadilha:** `context` não persiste entre eventos — use o banco/manager para estado.
|
|
472
|
+
- [ ] **Armadilha:** `aggregate`/`collection` (vindos do builder de IA) são ignorados — remova antes de salvar.
|
|
473
|
+
- [ ] **Pré-requisito de ambiente:** confirme que o `@ServerEndpoint` `/ws/{socket}` está de fato ativo (registro programático comentado em `Configuration.java`).
|
|
474
|
+
- [ ] Em deploy com múltiplos nós, lembre que broadcast não cruza o cluster.
|