api-ape 0.0.0 → 1.0.1
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/README.md +261 -0
- package/client/README.md +69 -0
- package/client/browser.js +17 -0
- package/client/connectSocket.js +260 -0
- package/dist/ape.js +454 -0
- package/example/ExpressJs/README.md +97 -0
- package/example/ExpressJs/api/message.js +11 -0
- package/example/ExpressJs/backend.js +37 -0
- package/example/ExpressJs/index.html +88 -0
- package/example/ExpressJs/package-lock.json +834 -0
- package/example/ExpressJs/package.json +10 -0
- package/example/ExpressJs/styles.css +128 -0
- package/example/NextJs/.dockerignore +29 -0
- package/example/NextJs/Dockerfile +52 -0
- package/example/NextJs/Dockerfile.dev +27 -0
- package/example/NextJs/README.md +113 -0
- package/example/NextJs/ape/client.js +66 -0
- package/example/NextJs/ape/embed.js +12 -0
- package/example/NextJs/ape/index.js +23 -0
- package/example/NextJs/ape/logic/chat.js +62 -0
- package/example/NextJs/ape/onConnect.js +69 -0
- package/example/NextJs/ape/onDisconnect.js +13 -0
- package/example/NextJs/ape/onError.js +9 -0
- package/example/NextJs/ape/onReceive.js +15 -0
- package/example/NextJs/ape/onSend.js +15 -0
- package/example/NextJs/api/message.js +44 -0
- package/example/NextJs/docker-compose.yml +22 -0
- package/example/NextJs/next-env.d.ts +5 -0
- package/example/NextJs/next.config.js +8 -0
- package/example/NextJs/package-lock.json +5107 -0
- package/example/NextJs/package.json +25 -0
- package/example/NextJs/pages/_app.tsx +6 -0
- package/example/NextJs/pages/index.tsx +182 -0
- package/example/NextJs/public/favicon.ico +0 -0
- package/example/NextJs/public/vercel.svg +4 -0
- package/example/NextJs/server.js +40 -0
- package/example/NextJs/styles/Chat.module.css +194 -0
- package/example/NextJs/styles/Home.module.css +129 -0
- package/example/NextJs/styles/globals.css +26 -0
- package/example/NextJs/tsconfig.json +20 -0
- package/example/README.md +66 -0
- package/index.d.ts +179 -0
- package/index.js +11 -0
- package/package.json +11 -4
- package/server/README.md +93 -0
- package/server/index.js +6 -0
- package/server/lib/broadcast.js +63 -0
- package/server/lib/loader.js +10 -0
- package/server/lib/main.js +23 -0
- package/server/lib/wiring.js +94 -0
- package/server/security/extractRootDomain.js +21 -0
- package/server/security/origin.js +13 -0
- package/server/security/reply.js +21 -0
- package/server/socket/open.js +10 -0
- package/server/socket/receive.js +66 -0
- package/server/socket/send.js +55 -0
- package/server/utils/deepRequire.js +45 -0
- package/server/utils/genId.js +24 -0
- package/todo.md +85 -0
- package/utils/jss.js +273 -0
- package/utils/jss.test.js +261 -0
- package/utils/messageHash.js +43 -0
- package/utils/messageHash.test.js +56 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
* {
|
|
2
|
+
box-sizing: border-box;
|
|
3
|
+
margin: 0;
|
|
4
|
+
padding: 0;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
body {
|
|
8
|
+
min-height: 100vh;
|
|
9
|
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
|
10
|
+
color: #fff;
|
|
11
|
+
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif;
|
|
12
|
+
display: flex;
|
|
13
|
+
justify-content: center;
|
|
14
|
+
padding: 2rem;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
chat-app {
|
|
18
|
+
max-width: 500px;
|
|
19
|
+
width: 100%;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.chat-container {
|
|
23
|
+
background: rgba(255, 255, 255, 0.05);
|
|
24
|
+
border-radius: 20px;
|
|
25
|
+
overflow: hidden;
|
|
26
|
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.header {
|
|
30
|
+
display: flex;
|
|
31
|
+
justify-content: space-between;
|
|
32
|
+
align-items: center;
|
|
33
|
+
padding: 1rem 1.5rem;
|
|
34
|
+
background: rgba(255, 255, 255, 0.1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.title {
|
|
38
|
+
font-size: 1.25rem;
|
|
39
|
+
font-weight: bold;
|
|
40
|
+
background: linear-gradient(90deg, #00d2ff, #3a7bd5);
|
|
41
|
+
-webkit-background-clip: text;
|
|
42
|
+
-webkit-text-fill-color: transparent;
|
|
43
|
+
background-clip: text;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.user-badge {
|
|
47
|
+
font-size: 0.85rem;
|
|
48
|
+
color: #0f0;
|
|
49
|
+
font-weight: bold;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.messages {
|
|
53
|
+
height: 350px;
|
|
54
|
+
overflow-y: auto;
|
|
55
|
+
padding: 1rem;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.messages::-webkit-scrollbar {
|
|
59
|
+
width: 6px;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.messages::-webkit-scrollbar-track {
|
|
63
|
+
background: rgba(255, 255, 255, 0.05);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.messages::-webkit-scrollbar-thumb {
|
|
67
|
+
background: rgba(255, 255, 255, 0.2);
|
|
68
|
+
border-radius: 3px;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.empty-state {
|
|
72
|
+
text-align: center;
|
|
73
|
+
color: #666;
|
|
74
|
+
margin-top: 140px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.message {
|
|
78
|
+
display: flex;
|
|
79
|
+
flex-direction: column;
|
|
80
|
+
gap: 0.25rem;
|
|
81
|
+
padding: 0.75rem 1rem;
|
|
82
|
+
margin-bottom: 0.5rem;
|
|
83
|
+
background: rgba(255, 255, 255, 0.05);
|
|
84
|
+
border-radius: 12px;
|
|
85
|
+
border-left: 3px solid #3a7bd5;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.message.mine {
|
|
89
|
+
background: rgba(0, 210, 255, 0.15);
|
|
90
|
+
border-left-color: #00d2ff;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.message .username {
|
|
94
|
+
color: #00d2ff;
|
|
95
|
+
font-size: 0.85rem;
|
|
96
|
+
font-weight: bold;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.message .text {
|
|
100
|
+
color: #fff;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.input-area {
|
|
104
|
+
display: flex;
|
|
105
|
+
gap: 0.5rem;
|
|
106
|
+
padding: 1rem;
|
|
107
|
+
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.message-input {
|
|
111
|
+
flex: 1;
|
|
112
|
+
padding: 0.75rem 1rem;
|
|
113
|
+
font-size: 1rem;
|
|
114
|
+
border: none;
|
|
115
|
+
border-radius: 50px;
|
|
116
|
+
background: rgba(255, 255, 255, 0.1);
|
|
117
|
+
color: #fff;
|
|
118
|
+
outline: none;
|
|
119
|
+
transition: background 0.2s;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.message-input::placeholder {
|
|
123
|
+
color: rgba(255, 255, 255, 0.5);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.message-input:focus {
|
|
127
|
+
background: rgba(255, 255, 255, 0.15);
|
|
128
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Dependencies
|
|
2
|
+
node_modules
|
|
3
|
+
|
|
4
|
+
# Next.js build output
|
|
5
|
+
.next
|
|
6
|
+
|
|
7
|
+
# Git
|
|
8
|
+
.git
|
|
9
|
+
.gitignore
|
|
10
|
+
|
|
11
|
+
# IDE
|
|
12
|
+
.idea
|
|
13
|
+
.vscode
|
|
14
|
+
*.swp
|
|
15
|
+
*.swo
|
|
16
|
+
|
|
17
|
+
# Debug
|
|
18
|
+
npm-debug.log*
|
|
19
|
+
yarn-debug.log*
|
|
20
|
+
yarn-error.log*
|
|
21
|
+
|
|
22
|
+
# Testing
|
|
23
|
+
coverage
|
|
24
|
+
|
|
25
|
+
# Misc
|
|
26
|
+
.DS_Store
|
|
27
|
+
*.pem
|
|
28
|
+
.env*.local
|
|
29
|
+
README.md
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Install dependencies only when needed
|
|
2
|
+
FROM node:18-alpine AS deps
|
|
3
|
+
RUN apk add --no-cache libc6-compat
|
|
4
|
+
WORKDIR /app
|
|
5
|
+
|
|
6
|
+
# Install dependencies based on the preferred package manager
|
|
7
|
+
COPY package.json package-lock.json* ./
|
|
8
|
+
RUN npm ci
|
|
9
|
+
|
|
10
|
+
# Rebuild the source code only when needed
|
|
11
|
+
FROM node:18-alpine AS builder
|
|
12
|
+
WORKDIR /app
|
|
13
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
14
|
+
COPY . .
|
|
15
|
+
|
|
16
|
+
# Next.js collects completely anonymous telemetry data about general usage.
|
|
17
|
+
# Learn more here: https://nextjs.org/telemetry
|
|
18
|
+
# Uncomment the following line in case you want to disable telemetry during the build.
|
|
19
|
+
# ENV NEXT_TELEMETRY_DISABLED 1
|
|
20
|
+
|
|
21
|
+
RUN npm run build
|
|
22
|
+
|
|
23
|
+
# Production image, copy all the files and run next
|
|
24
|
+
FROM node:18-alpine AS runner
|
|
25
|
+
WORKDIR /app
|
|
26
|
+
|
|
27
|
+
ENV NODE_ENV production
|
|
28
|
+
# Uncomment the following line in case you want to disable telemetry during runtime.
|
|
29
|
+
# ENV NEXT_TELEMETRY_DISABLED 1
|
|
30
|
+
|
|
31
|
+
RUN addgroup --system --gid 1001 nodejs
|
|
32
|
+
RUN adduser --system --uid 1001 nextjs
|
|
33
|
+
|
|
34
|
+
COPY --from=builder /app/public ./public
|
|
35
|
+
|
|
36
|
+
# Set the correct permission for prerender cache
|
|
37
|
+
RUN mkdir .next
|
|
38
|
+
RUN chown nextjs:nodejs .next
|
|
39
|
+
|
|
40
|
+
# Automatically leverage output traces to reduce image size
|
|
41
|
+
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
|
42
|
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
|
43
|
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
|
44
|
+
|
|
45
|
+
USER nextjs
|
|
46
|
+
|
|
47
|
+
EXPOSE 3000
|
|
48
|
+
|
|
49
|
+
ENV PORT 3000
|
|
50
|
+
ENV HOSTNAME "0.0.0.0"
|
|
51
|
+
|
|
52
|
+
CMD ["node", "server.js"]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Development Dockerfile with hot reload support
|
|
2
|
+
# Context: api-ape root (not example/NextJs)
|
|
3
|
+
FROM node:18-alpine
|
|
4
|
+
|
|
5
|
+
WORKDIR /api-ape
|
|
6
|
+
|
|
7
|
+
# First install api-ape root dependencies (ws, ua-parser-js, etc.)
|
|
8
|
+
COPY package.json package-lock.json* ./
|
|
9
|
+
RUN npm install
|
|
10
|
+
|
|
11
|
+
# Then install NextJs example dependencies
|
|
12
|
+
WORKDIR /api-ape/example/NextJs
|
|
13
|
+
COPY example/NextJs/package.json example/NextJs/package-lock.json* ./
|
|
14
|
+
RUN npm install
|
|
15
|
+
|
|
16
|
+
# Copy all source files
|
|
17
|
+
WORKDIR /api-ape
|
|
18
|
+
COPY . .
|
|
19
|
+
|
|
20
|
+
# Back to NextJs example
|
|
21
|
+
WORKDIR /api-ape/example/NextJs
|
|
22
|
+
|
|
23
|
+
# Expose port
|
|
24
|
+
EXPOSE 3000
|
|
25
|
+
|
|
26
|
+
# Run in development mode
|
|
27
|
+
CMD ["npm", "run", "dev"]
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# 🦍 NextJs — Complete Example
|
|
2
|
+
|
|
3
|
+
A full-featured real-time chat application with Next.js and api-ape.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
npm run dev
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Open http://localhost:3000 in your browser.
|
|
13
|
+
|
|
14
|
+
## Docker
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
docker-compose up --build
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Project Structure
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
NextJs/
|
|
24
|
+
├── server.js # Custom Next.js server with api-ape
|
|
25
|
+
├── api/
|
|
26
|
+
│ └── message.js # Message controller
|
|
27
|
+
├── ape/
|
|
28
|
+
│ ├── index.js # Ape exports
|
|
29
|
+
│ ├── client.js # Browser client wrapper
|
|
30
|
+
│ ├── onConnect.js # Connection lifecycle
|
|
31
|
+
│ ├── onDisconnect.js # Disconnect handler
|
|
32
|
+
│ ├── onReceive.js # Message logging
|
|
33
|
+
│ ├── onSend.js # Send logging
|
|
34
|
+
│ ├── onError.js # Error handling
|
|
35
|
+
│ └── embed.js # Embedded context values
|
|
36
|
+
├── pages/
|
|
37
|
+
│ └── index.tsx # Chat UI
|
|
38
|
+
└── styles/
|
|
39
|
+
└── Chat.module.css # Chat styling
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Features
|
|
43
|
+
|
|
44
|
+
- **Custom Server** — Express + Next.js with api-ape integration
|
|
45
|
+
- **Connection Lifecycle** — onConnect, onDisconnect, onReceive, onSend hooks
|
|
46
|
+
- **User Presence** — Track online users count
|
|
47
|
+
- **Message History** — New users receive chat history
|
|
48
|
+
- **React Integration** — Hooks-based client usage
|
|
49
|
+
- **Docker Support** — Production-ready containerization
|
|
50
|
+
|
|
51
|
+
## How It Works
|
|
52
|
+
|
|
53
|
+
### Server (server.js)
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
const express = require('express')
|
|
57
|
+
const next = require('next')
|
|
58
|
+
const ape = require('api-ape')
|
|
59
|
+
const { onConnect } = require('./ape/onConnect')
|
|
60
|
+
|
|
61
|
+
const app = next({ dev: true })
|
|
62
|
+
const server = express()
|
|
63
|
+
|
|
64
|
+
ape(server, { where: 'api', onConnent: onConnect })
|
|
65
|
+
server.all('*', app.getRequestHandler())
|
|
66
|
+
server.listen(3000)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Connection Lifecycle (ape/onConnect.js)
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
module.exports.onConnect = (socket, req, send) => ({
|
|
73
|
+
embed: { userId: generateId() },
|
|
74
|
+
onReceive: (queryId, data, type) => { ... },
|
|
75
|
+
onSend: (data, type) => { ... },
|
|
76
|
+
onDisconnent: () => { ... }
|
|
77
|
+
})
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### React Client (pages/index.tsx)
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
npm i api-ape
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```jsx
|
|
87
|
+
import ape from 'api-ape'
|
|
88
|
+
|
|
89
|
+
// Configure and connect
|
|
90
|
+
ape.configure({ port: 3000 })
|
|
91
|
+
const { sender, setOnReciver } = ape()
|
|
92
|
+
ape.autoReconnect()
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
setOnReciver('message', ({ data }) => {
|
|
96
|
+
setMessages(prev => [...prev, data.message])
|
|
97
|
+
})
|
|
98
|
+
}, [])
|
|
99
|
+
|
|
100
|
+
// Send message
|
|
101
|
+
sender.message({ user, text }).then(response => { ... })
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Key Concepts Demonstrated
|
|
105
|
+
|
|
106
|
+
| Concept | File |
|
|
107
|
+
|---------|------|
|
|
108
|
+
| Custom Next.js server | `server.js` |
|
|
109
|
+
| Connection lifecycle hooks | `ape/onConnect.js` |
|
|
110
|
+
| Embedded context values | `ape/embed.js` |
|
|
111
|
+
| React hooks integration | `pages/index.tsx` |
|
|
112
|
+
| Client wrapper | `ape/client.js` |
|
|
113
|
+
| Message validation | `api/message.js` |
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* api-ape client singleton
|
|
3
|
+
* Initializes once and is ready for use throughout the app
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
let apeClient = null
|
|
7
|
+
let isConnecting = false
|
|
8
|
+
let connectionPromise = null
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get the api-ape client - initializes on first call
|
|
12
|
+
*/
|
|
13
|
+
export async function getApeClient() {
|
|
14
|
+
if (apeClient) {
|
|
15
|
+
return apeClient
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (isConnecting) {
|
|
19
|
+
return connectionPromise
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
isConnecting = true
|
|
23
|
+
connectionPromise = initClient()
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
apeClient = await connectionPromise
|
|
27
|
+
return apeClient
|
|
28
|
+
} finally {
|
|
29
|
+
isConnecting = false
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Initialize the api-ape client
|
|
35
|
+
*/
|
|
36
|
+
async function initClient() {
|
|
37
|
+
if (typeof window === 'undefined') {
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const module = await import('api-ape/client/connectSocket')
|
|
42
|
+
const connectSocket = module.default
|
|
43
|
+
|
|
44
|
+
// Configure for current port
|
|
45
|
+
const port = window.location.port || (window.location.protocol === 'https:' ? 443 : 80)
|
|
46
|
+
connectSocket.configure({ port: parseInt(port, 10) })
|
|
47
|
+
|
|
48
|
+
// Connect and get sender/receiver
|
|
49
|
+
const { sender, setOnReciver } = connectSocket()
|
|
50
|
+
|
|
51
|
+
// Enable auto-reconnect
|
|
52
|
+
connectSocket.autoReconnect()
|
|
53
|
+
|
|
54
|
+
console.log('🦍 api-ape client initialized')
|
|
55
|
+
|
|
56
|
+
return { sender, setOnReciver, connectSocket }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Check if client is connected
|
|
61
|
+
*/
|
|
62
|
+
export function isConnected() {
|
|
63
|
+
return apeClient !== null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export default getApeClient
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* api-ape handlers - main export
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { onConnect, history, online } = require('./onConnect')
|
|
6
|
+
const { createEmbed } = require('./embed')
|
|
7
|
+
const { onReceive } = require('./onReceive')
|
|
8
|
+
const { onSend } = require('./onSend')
|
|
9
|
+
const { onError } = require('./onError')
|
|
10
|
+
const { onDisconnect } = require('./onDisconnect')
|
|
11
|
+
const chat = require('./logic/chat')
|
|
12
|
+
|
|
13
|
+
module.exports = {
|
|
14
|
+
onConnect,
|
|
15
|
+
history,
|
|
16
|
+
online,
|
|
17
|
+
createEmbed,
|
|
18
|
+
onReceive,
|
|
19
|
+
onSend,
|
|
20
|
+
onError,
|
|
21
|
+
onDisconnect,
|
|
22
|
+
chat
|
|
23
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat controller for api-ape
|
|
3
|
+
* Uses this.broadcastOthers from api-ape
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// In-memory message store
|
|
7
|
+
const messages = []
|
|
8
|
+
const MAX_MESSAGES = 100
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Message controller - called when client sends type="message"
|
|
12
|
+
* Uses this.broadcastOthers to send to all OTHER clients
|
|
13
|
+
*/
|
|
14
|
+
function message(data) {
|
|
15
|
+
const { user, text } = data
|
|
16
|
+
|
|
17
|
+
if (!user || !text) {
|
|
18
|
+
throw new Error('Missing user or text')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const msg = {
|
|
22
|
+
user,
|
|
23
|
+
text,
|
|
24
|
+
time: new Date().toISOString()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Store message
|
|
28
|
+
messages.push(msg)
|
|
29
|
+
if (messages.length > MAX_MESSAGES) {
|
|
30
|
+
messages.shift()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Broadcast to all OTHER clients (exclude sender)
|
|
34
|
+
// this.broadcastOthers is provided by api-ape!
|
|
35
|
+
this.broadcastOthers('message', { message: msg })
|
|
36
|
+
|
|
37
|
+
// Return to sender (fulfills promise)
|
|
38
|
+
return { ok: true, message: msg }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get message history
|
|
43
|
+
*/
|
|
44
|
+
function history() {
|
|
45
|
+
return messages.slice(-50)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get history controller - called when client sends type="history"
|
|
50
|
+
*/
|
|
51
|
+
function getHistory() {
|
|
52
|
+
return {
|
|
53
|
+
history: history(),
|
|
54
|
+
users: this.online()
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = {
|
|
59
|
+
message,
|
|
60
|
+
history: getHistory,
|
|
61
|
+
_history: history // internal use
|
|
62
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* api-ape onConnect handler
|
|
3
|
+
* Creates the handlers object returned from onConnent
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { createEmbed } = require('./embed')
|
|
7
|
+
const { onReceive } = require('./onReceive')
|
|
8
|
+
const { onSend } = require('./onSend')
|
|
9
|
+
const { onError } = require('./onError')
|
|
10
|
+
const { broadcast, online } = require('api-ape/server/lib/broadcast')
|
|
11
|
+
|
|
12
|
+
// Get message history from the message controller
|
|
13
|
+
function getHistory() {
|
|
14
|
+
try {
|
|
15
|
+
const messageController = require('../api/message')
|
|
16
|
+
return messageController.getHistory ? messageController.getHistory() : []
|
|
17
|
+
} catch (e) {
|
|
18
|
+
return []
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function onConnect(socket, req, send) {
|
|
23
|
+
const clientID = send.toString()
|
|
24
|
+
console.log(`🦍 Client connected: ${clientID}`)
|
|
25
|
+
|
|
26
|
+
const embed = createEmbed(clientID, req.headers?.['x-session-id'])
|
|
27
|
+
|
|
28
|
+
// Send init message with history and user count after a tiny delay
|
|
29
|
+
// (to ensure client is ready to receive)
|
|
30
|
+
setTimeout(() => {
|
|
31
|
+
console.log(`📤 Sending init to ${clientID}, users: ${online()}`)
|
|
32
|
+
try {
|
|
33
|
+
send('init', {
|
|
34
|
+
history: getHistory(),
|
|
35
|
+
users: online()
|
|
36
|
+
})
|
|
37
|
+
console.log(`✅ Init sent to ${clientID}`)
|
|
38
|
+
} catch (e) {
|
|
39
|
+
console.error(`❌ Failed to send init:`, e)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Broadcast updated user count to all clients
|
|
43
|
+
broadcast('users', { count: online() })
|
|
44
|
+
}, 100)
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
embed,
|
|
48
|
+
|
|
49
|
+
onReceive: (queryId, payload, type) =>
|
|
50
|
+
onReceive(clientID, queryId, payload, type),
|
|
51
|
+
|
|
52
|
+
onSend: (payload, type) =>
|
|
53
|
+
onSend(clientID, payload, type),
|
|
54
|
+
|
|
55
|
+
onError: (errStr) =>
|
|
56
|
+
onError(clientID, errStr),
|
|
57
|
+
|
|
58
|
+
onDisconnent: () => {
|
|
59
|
+
console.info(`👋 Disconnected [${clientID}]`)
|
|
60
|
+
// Broadcast updated user count after disconnect
|
|
61
|
+
// Use setTimeout to ensure client is removed first
|
|
62
|
+
setTimeout(() => {
|
|
63
|
+
broadcast('users', { count: online() })
|
|
64
|
+
}, 50)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = { onConnect }
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* api-ape onDisconnect handler
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { broadcast, online } = require('./logic/chat')
|
|
6
|
+
|
|
7
|
+
function onDisconnect(clientID, unsubscribe) {
|
|
8
|
+
console.info(`👋 Disconnected [${clientID}]`)
|
|
9
|
+
unsubscribe()
|
|
10
|
+
broadcast('users', { count: online() })
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
module.exports = { onDisconnect }
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* api-ape onReceive handler
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
function onReceive(clientID, queryId, payload, type) {
|
|
6
|
+
console.log(`📥 [${clientID}] ${type}:`, JSON.stringify(payload).slice(0, 50))
|
|
7
|
+
|
|
8
|
+
return (err, result) => {
|
|
9
|
+
if (err) {
|
|
10
|
+
console.error(`❌ [${clientID}] Error:`, err.message)
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = { onReceive }
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* api-ape onSend handler
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
function onSend(clientID, payload, type) {
|
|
6
|
+
console.log(`📤 [${clientID}] ${type}`)
|
|
7
|
+
|
|
8
|
+
return (err, result) => {
|
|
9
|
+
if (err) {
|
|
10
|
+
console.error(`❌ [${clientID}] Send failed:`, err.message)
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = { onSend }
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message controller for api-ape
|
|
3
|
+
* Called when client sends type="message"
|
|
4
|
+
*
|
|
5
|
+
* Uses this.broadcastOthers from api-ape to broadcast to all other clients
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// In-memory message store
|
|
9
|
+
const messages = []
|
|
10
|
+
const MAX_MESSAGES = 100
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Message handler - receives { user, text } from client
|
|
14
|
+
* Broadcasts to all OTHER clients, returns to sender
|
|
15
|
+
*/
|
|
16
|
+
module.exports = function message(data) {
|
|
17
|
+
const { user, text } = data
|
|
18
|
+
|
|
19
|
+
if (!user || !text) {
|
|
20
|
+
throw new Error('Missing user or text')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const msg = {
|
|
24
|
+
user,
|
|
25
|
+
text,
|
|
26
|
+
time: new Date().toISOString()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Store message
|
|
30
|
+
messages.push(msg)
|
|
31
|
+
if (messages.length > MAX_MESSAGES) {
|
|
32
|
+
messages.shift()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Broadcast to all OTHER clients (exclude sender)
|
|
36
|
+
// this.broadcastOthers is provided by api-ape!
|
|
37
|
+
this.broadcastOthers('message', { message: msg })
|
|
38
|
+
|
|
39
|
+
// Return to sender (fulfills promise)
|
|
40
|
+
return { ok: true, message: msg }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Export history for other uses
|
|
44
|
+
module.exports.getHistory = () => messages.slice(-50)
|