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,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "apiape_example",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "node server.js",
|
|
7
|
+
"build": "next build",
|
|
8
|
+
"start": "node server.js",
|
|
9
|
+
"lint": "next lint"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@types/node": "18.11.13",
|
|
13
|
+
"@types/react": "18.0.26",
|
|
14
|
+
"@types/react-dom": "18.0.9",
|
|
15
|
+
"api-ape": "file:../..",
|
|
16
|
+
"eslint": "8.29.0",
|
|
17
|
+
"eslint-config-next": "13.0.6",
|
|
18
|
+
"express": "^4.18.2",
|
|
19
|
+
"next": "13.0.6",
|
|
20
|
+
"react": "18.2.0",
|
|
21
|
+
"react-dom": "18.2.0",
|
|
22
|
+
"typescript": "4.9.4",
|
|
23
|
+
"ws": "^8.14.0"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import Head from 'next/head'
|
|
2
|
+
import { useState, useEffect, useRef } from 'react'
|
|
3
|
+
import styles from '../styles/Chat.module.css'
|
|
4
|
+
import { getApeClient } from '../ape/client'
|
|
5
|
+
|
|
6
|
+
export default function Home() {
|
|
7
|
+
const [messages, setMessages] = useState([])
|
|
8
|
+
const [input, setInput] = useState('')
|
|
9
|
+
const [username, setUsername] = useState('')
|
|
10
|
+
const [joined, setJoined] = useState(false)
|
|
11
|
+
const [userCount, setUserCount] = useState(0)
|
|
12
|
+
const [sending, setSending] = useState(false)
|
|
13
|
+
const [connected, setConnected] = useState(false)
|
|
14
|
+
const messagesEndRef = useRef(null)
|
|
15
|
+
const apiRef = useRef(null) // The sender proxy
|
|
16
|
+
|
|
17
|
+
// Initialize api-ape client on mount (before join)
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (typeof window === 'undefined') return
|
|
20
|
+
|
|
21
|
+
getApeClient().then((client) => {
|
|
22
|
+
if (!client) return
|
|
23
|
+
|
|
24
|
+
// Store the sender proxy - ready to use!
|
|
25
|
+
apiRef.current = client.sender
|
|
26
|
+
setConnected(true)
|
|
27
|
+
console.log('🦍 api-ape ready')
|
|
28
|
+
|
|
29
|
+
// Set up message listeners
|
|
30
|
+
client.setOnReciver('init', ({ data }) => {
|
|
31
|
+
setMessages(data.history || [])
|
|
32
|
+
setUserCount(data.users || 0)
|
|
33
|
+
console.log('🦍 Initialized')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
client.setOnReciver('message', ({ data }) => {
|
|
37
|
+
setMessages(prev => [...prev, data.message])
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
client.setOnReciver('users', ({ data }) => {
|
|
41
|
+
setUserCount(data.count)
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
}, [])
|
|
45
|
+
|
|
46
|
+
// Auto-scroll to bottom
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
|
49
|
+
}, [messages])
|
|
50
|
+
|
|
51
|
+
// Send message using the proxy pattern
|
|
52
|
+
// api.message(data) -> sends type="message" with data
|
|
53
|
+
// Returns a Promise - server can reply with matching queryId
|
|
54
|
+
const sendMessage = (e) => {
|
|
55
|
+
e.preventDefault()
|
|
56
|
+
if (!input.trim() || !apiRef.current || sending) return
|
|
57
|
+
|
|
58
|
+
const api = apiRef.current
|
|
59
|
+
|
|
60
|
+
setSending(true)
|
|
61
|
+
|
|
62
|
+
// api.message({ user, text }) - "message" is the type!
|
|
63
|
+
// createdAt is auto-added by client
|
|
64
|
+
// queryId hash is auto-generated for request/response matching
|
|
65
|
+
api.message({ user: username, text: input })
|
|
66
|
+
.then((response) => {
|
|
67
|
+
// Server replied with matching queryId
|
|
68
|
+
// Add our own message to the list (we sent it, so we add it)
|
|
69
|
+
if (response?.message) {
|
|
70
|
+
setMessages(prev => [...prev, response.message])
|
|
71
|
+
}
|
|
72
|
+
setSending(false)
|
|
73
|
+
})
|
|
74
|
+
.catch((err) => {
|
|
75
|
+
console.error('Send failed:', err)
|
|
76
|
+
setSending(false)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
setInput('')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const handleJoin = (e) => {
|
|
83
|
+
e.preventDefault()
|
|
84
|
+
if (username.trim()) {
|
|
85
|
+
setJoined(true)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div className={styles.container}>
|
|
91
|
+
<Head>
|
|
92
|
+
<title>🦍 api-ape Chat</title>
|
|
93
|
+
<meta name="description" content="Real-time WebSocket chat using api-ape" />
|
|
94
|
+
</Head>
|
|
95
|
+
|
|
96
|
+
<main className={styles.main}>
|
|
97
|
+
<h1 className={styles.title}>
|
|
98
|
+
🦍 <span className={styles.gradient}>api-ape</span> Chat
|
|
99
|
+
</h1>
|
|
100
|
+
<p className={styles.subtitle}>
|
|
101
|
+
{connected ? '✅ Connected' : '⏳ Connecting...'} • Pure WebSocket
|
|
102
|
+
</p>
|
|
103
|
+
|
|
104
|
+
{!joined ? (
|
|
105
|
+
<form onSubmit={handleJoin} className={styles.joinForm}>
|
|
106
|
+
<input
|
|
107
|
+
type="text"
|
|
108
|
+
placeholder="Enter your name..."
|
|
109
|
+
value={username}
|
|
110
|
+
onChange={(e) => setUsername(e.target.value)}
|
|
111
|
+
className={styles.input}
|
|
112
|
+
autoFocus
|
|
113
|
+
/>
|
|
114
|
+
<button type="submit" className={styles.button} disabled={!connected}>
|
|
115
|
+
{connected ? 'Join Chat →' : 'Connecting...'}
|
|
116
|
+
</button>
|
|
117
|
+
</form>
|
|
118
|
+
) : (
|
|
119
|
+
<div className={styles.chatContainer}>
|
|
120
|
+
<div className={styles.header}>
|
|
121
|
+
<span>💬 {username}</span>
|
|
122
|
+
<span className={styles.userCount}>
|
|
123
|
+
🟢 {userCount} online
|
|
124
|
+
</span>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<div className={styles.messages}>
|
|
128
|
+
{messages.length === 0 && (
|
|
129
|
+
<p className={styles.emptyState}>No messages yet. Say hi! 👋</p>
|
|
130
|
+
)}
|
|
131
|
+
{messages.map((msg, i) => (
|
|
132
|
+
<div
|
|
133
|
+
key={i}
|
|
134
|
+
className={`${styles.message} ${msg.user === username ? styles.myMessage : ''}`}
|
|
135
|
+
>
|
|
136
|
+
<strong className={styles.username}>{msg.user}</strong>
|
|
137
|
+
<span>{msg.text}</span>
|
|
138
|
+
<span className={styles.time}>
|
|
139
|
+
{new Date(msg.time).toLocaleTimeString()}
|
|
140
|
+
</span>
|
|
141
|
+
</div>
|
|
142
|
+
))}
|
|
143
|
+
<div ref={messagesEndRef} />
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<form onSubmit={sendMessage} className={styles.inputForm}>
|
|
147
|
+
<input
|
|
148
|
+
type="text"
|
|
149
|
+
placeholder="Type a message..."
|
|
150
|
+
value={input}
|
|
151
|
+
onChange={(e) => setInput(e.target.value)}
|
|
152
|
+
className={styles.messageInput}
|
|
153
|
+
disabled={sending}
|
|
154
|
+
autoFocus
|
|
155
|
+
/>
|
|
156
|
+
<button type="submit" className={styles.sendButton} disabled={sending}>
|
|
157
|
+
{sending ? '...' : 'Send'}
|
|
158
|
+
</button>
|
|
159
|
+
</form>
|
|
160
|
+
</div>
|
|
161
|
+
)}
|
|
162
|
+
|
|
163
|
+
<div className={styles.codeSection}>
|
|
164
|
+
<h3 className={styles.codeTitle}>✨ api-ape Proxy Pattern</h3>
|
|
165
|
+
<pre className={styles.code}>
|
|
166
|
+
{`// Sender is a Proxy - prop name = type
|
|
167
|
+
const api = client.sender
|
|
168
|
+
|
|
169
|
+
// Send with Promise - queryId auto-matched
|
|
170
|
+
setSending(true)
|
|
171
|
+
api.message({ user, text })
|
|
172
|
+
.then(() => setSending(false))
|
|
173
|
+
.catch(err => console.error(err))
|
|
174
|
+
|
|
175
|
+
// createdAt auto-added by client
|
|
176
|
+
// queryId hash matches request/response`}
|
|
177
|
+
</pre>
|
|
178
|
+
</div>
|
|
179
|
+
</main>
|
|
180
|
+
</div>
|
|
181
|
+
)
|
|
182
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
|
|
2
|
+
xmlns="http://www.w3.org/2000/svg">
|
|
3
|
+
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Next.js server using actual api-ape library with Express
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const express = require('express')
|
|
6
|
+
const next = require('next')
|
|
7
|
+
const ape = require('api-ape')
|
|
8
|
+
const { onConnect } = require('./ape/onConnect')
|
|
9
|
+
|
|
10
|
+
const dev = process.env.NODE_ENV !== 'production'
|
|
11
|
+
const hostname = '0.0.0.0'
|
|
12
|
+
const port = parseInt(process.env.PORT, 10) || 3000
|
|
13
|
+
|
|
14
|
+
const app = next({ dev, hostname, port })
|
|
15
|
+
const handle = app.getRequestHandler()
|
|
16
|
+
|
|
17
|
+
app.prepare().then(() => {
|
|
18
|
+
const server = express()
|
|
19
|
+
|
|
20
|
+
// Initialize api-ape - it handles EVERYTHING
|
|
21
|
+
// Developer never touches WebSocket!
|
|
22
|
+
ape(server, { where: 'api', onConnent: onConnect })
|
|
23
|
+
|
|
24
|
+
// Let Next.js handle all other routes
|
|
25
|
+
server.all('*', (req, res) => {
|
|
26
|
+
return handle(req, res)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
server.listen(port, () => {
|
|
30
|
+
console.log(`
|
|
31
|
+
╔═══════════════════════════════════════════════════════╗
|
|
32
|
+
║ 🦍 api-ape NextJS Demo ║
|
|
33
|
+
╠═══════════════════════════════════════════════════════╣
|
|
34
|
+
║ HTTP: http://localhost:${port}/ ║
|
|
35
|
+
║ WebSocket: ws://localhost:${port}/api/ape ║
|
|
36
|
+
║ ape(app, { where: "api", onConnent }) ║
|
|
37
|
+
╚═══════════════════════════════════════════════════════╝
|
|
38
|
+
`)
|
|
39
|
+
})
|
|
40
|
+
})
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
.container {
|
|
2
|
+
min-height: 100vh;
|
|
3
|
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
|
4
|
+
color: #fff;
|
|
5
|
+
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.main {
|
|
9
|
+
max-width: 600px;
|
|
10
|
+
margin: 0 auto;
|
|
11
|
+
padding: 2rem;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.title {
|
|
15
|
+
font-size: 2.5rem;
|
|
16
|
+
text-align: center;
|
|
17
|
+
margin-bottom: 0.5rem;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.gradient {
|
|
21
|
+
background: linear-gradient(90deg, #00d2ff, #3a7bd5);
|
|
22
|
+
-webkit-background-clip: text;
|
|
23
|
+
-webkit-text-fill-color: transparent;
|
|
24
|
+
background-clip: text;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.subtitle {
|
|
28
|
+
text-align: center;
|
|
29
|
+
color: #0f0;
|
|
30
|
+
margin-bottom: 2rem;
|
|
31
|
+
font-weight: bold;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.joinForm {
|
|
35
|
+
display: flex;
|
|
36
|
+
gap: 1rem;
|
|
37
|
+
justify-content: center;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.input {
|
|
41
|
+
padding: 1rem 1.5rem;
|
|
42
|
+
font-size: 1rem;
|
|
43
|
+
border: none;
|
|
44
|
+
border-radius: 50px;
|
|
45
|
+
background: rgba(255, 255, 255, 0.1);
|
|
46
|
+
color: #fff;
|
|
47
|
+
outline: none;
|
|
48
|
+
width: 250px;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.input::placeholder {
|
|
52
|
+
color: rgba(255, 255, 255, 0.5);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.button {
|
|
56
|
+
padding: 1rem 2rem;
|
|
57
|
+
font-size: 1rem;
|
|
58
|
+
border: none;
|
|
59
|
+
border-radius: 50px;
|
|
60
|
+
background: linear-gradient(90deg, #00d2ff, #3a7bd5);
|
|
61
|
+
color: #fff;
|
|
62
|
+
cursor: pointer;
|
|
63
|
+
font-weight: bold;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.button:hover {
|
|
67
|
+
opacity: 0.9;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.chatContainer {
|
|
71
|
+
background: rgba(255, 255, 255, 0.05);
|
|
72
|
+
border-radius: 20px;
|
|
73
|
+
overflow: hidden;
|
|
74
|
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.header {
|
|
78
|
+
display: flex;
|
|
79
|
+
justify-content: space-between;
|
|
80
|
+
padding: 1rem 1.5rem;
|
|
81
|
+
background: rgba(255, 255, 255, 0.1);
|
|
82
|
+
font-weight: bold;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.userCount {
|
|
86
|
+
font-size: 0.85rem;
|
|
87
|
+
color: #0f0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.messages {
|
|
91
|
+
height: 300px;
|
|
92
|
+
overflow-y: auto;
|
|
93
|
+
padding: 1rem;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.emptyState {
|
|
97
|
+
text-align: center;
|
|
98
|
+
color: #666;
|
|
99
|
+
margin-top: 120px;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.message {
|
|
103
|
+
display: flex;
|
|
104
|
+
flex-direction: column;
|
|
105
|
+
gap: 0.25rem;
|
|
106
|
+
padding: 0.75rem 1rem;
|
|
107
|
+
margin-bottom: 0.5rem;
|
|
108
|
+
background: rgba(255, 255, 255, 0.05);
|
|
109
|
+
border-radius: 12px;
|
|
110
|
+
border-left: 3px solid #3a7bd5;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.myMessage {
|
|
114
|
+
background: rgba(0, 210, 255, 0.15);
|
|
115
|
+
border-left: 3px solid #00d2ff;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.username {
|
|
119
|
+
color: #00d2ff;
|
|
120
|
+
font-size: 0.85rem;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.time {
|
|
124
|
+
color: #666;
|
|
125
|
+
font-size: 0.7rem;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.inputForm {
|
|
129
|
+
display: flex;
|
|
130
|
+
gap: 0.5rem;
|
|
131
|
+
padding: 1rem;
|
|
132
|
+
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.messageInput {
|
|
136
|
+
flex: 1;
|
|
137
|
+
padding: 0.75rem 1rem;
|
|
138
|
+
font-size: 1rem;
|
|
139
|
+
border: none;
|
|
140
|
+
border-radius: 50px;
|
|
141
|
+
background: rgba(255, 255, 255, 0.1);
|
|
142
|
+
color: #fff;
|
|
143
|
+
outline: none;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.messageInput::placeholder {
|
|
147
|
+
color: rgba(255, 255, 255, 0.5);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.sendButton {
|
|
151
|
+
padding: 0.75rem 1.5rem;
|
|
152
|
+
font-size: 1rem;
|
|
153
|
+
border: none;
|
|
154
|
+
border-radius: 50px;
|
|
155
|
+
background: #3a7bd5;
|
|
156
|
+
color: #fff;
|
|
157
|
+
cursor: pointer;
|
|
158
|
+
font-weight: bold;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.sendButton:hover {
|
|
162
|
+
opacity: 0.9;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.codeSection {
|
|
166
|
+
margin-top: 2rem;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.codeTitle {
|
|
170
|
+
margin-bottom: 0.5rem;
|
|
171
|
+
color: #0f0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.code {
|
|
175
|
+
background: rgba(0, 0, 0, 0.4);
|
|
176
|
+
padding: 1.5rem;
|
|
177
|
+
border-radius: 12px;
|
|
178
|
+
font-size: 0.75rem;
|
|
179
|
+
color: #0f0;
|
|
180
|
+
overflow: auto;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.clientInfo {
|
|
184
|
+
text-align: center;
|
|
185
|
+
margin-top: 1rem;
|
|
186
|
+
font-size: 0.85rem;
|
|
187
|
+
color: #888;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.clientInfo code {
|
|
191
|
+
background: rgba(0, 0, 0, 0.3);
|
|
192
|
+
padding: 0.2rem 0.5rem;
|
|
193
|
+
border-radius: 4px;
|
|
194
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
.container {
|
|
2
|
+
padding: 0 2rem;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.main {
|
|
6
|
+
min-height: 100vh;
|
|
7
|
+
padding: 4rem 0;
|
|
8
|
+
flex: 1;
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
justify-content: center;
|
|
12
|
+
align-items: center;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.footer {
|
|
16
|
+
display: flex;
|
|
17
|
+
flex: 1;
|
|
18
|
+
padding: 2rem 0;
|
|
19
|
+
border-top: 1px solid #eaeaea;
|
|
20
|
+
justify-content: center;
|
|
21
|
+
align-items: center;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.footer a {
|
|
25
|
+
display: flex;
|
|
26
|
+
justify-content: center;
|
|
27
|
+
align-items: center;
|
|
28
|
+
flex-grow: 1;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.title a {
|
|
32
|
+
color: #0070f3;
|
|
33
|
+
text-decoration: none;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.title a:hover,
|
|
37
|
+
.title a:focus,
|
|
38
|
+
.title a:active {
|
|
39
|
+
text-decoration: underline;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.title {
|
|
43
|
+
margin: 0;
|
|
44
|
+
line-height: 1.15;
|
|
45
|
+
font-size: 4rem;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.title,
|
|
49
|
+
.description {
|
|
50
|
+
text-align: center;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.description {
|
|
54
|
+
margin: 4rem 0;
|
|
55
|
+
line-height: 1.5;
|
|
56
|
+
font-size: 1.5rem;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.code {
|
|
60
|
+
background: #fafafa;
|
|
61
|
+
border-radius: 5px;
|
|
62
|
+
padding: 0.75rem;
|
|
63
|
+
font-size: 1.1rem;
|
|
64
|
+
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
|
|
65
|
+
Bitstream Vera Sans Mono, Courier New, monospace;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.grid {
|
|
69
|
+
display: flex;
|
|
70
|
+
align-items: center;
|
|
71
|
+
justify-content: center;
|
|
72
|
+
flex-wrap: wrap;
|
|
73
|
+
max-width: 800px;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.card {
|
|
77
|
+
margin: 1rem;
|
|
78
|
+
padding: 1.5rem;
|
|
79
|
+
text-align: left;
|
|
80
|
+
color: inherit;
|
|
81
|
+
text-decoration: none;
|
|
82
|
+
border: 1px solid #eaeaea;
|
|
83
|
+
border-radius: 10px;
|
|
84
|
+
transition: color 0.15s ease, border-color 0.15s ease;
|
|
85
|
+
max-width: 300px;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.card:hover,
|
|
89
|
+
.card:focus,
|
|
90
|
+
.card:active {
|
|
91
|
+
color: #0070f3;
|
|
92
|
+
border-color: #0070f3;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.card h2 {
|
|
96
|
+
margin: 0 0 1rem 0;
|
|
97
|
+
font-size: 1.5rem;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.card p {
|
|
101
|
+
margin: 0;
|
|
102
|
+
font-size: 1.25rem;
|
|
103
|
+
line-height: 1.5;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.logo {
|
|
107
|
+
height: 1em;
|
|
108
|
+
margin-left: 0.5rem;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@media (max-width: 600px) {
|
|
112
|
+
.grid {
|
|
113
|
+
width: 100%;
|
|
114
|
+
flex-direction: column;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@media (prefers-color-scheme: dark) {
|
|
119
|
+
.card,
|
|
120
|
+
.footer {
|
|
121
|
+
border-color: #222;
|
|
122
|
+
}
|
|
123
|
+
.code {
|
|
124
|
+
background: #111;
|
|
125
|
+
}
|
|
126
|
+
.logo img {
|
|
127
|
+
filter: invert(1);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
html,
|
|
2
|
+
body {
|
|
3
|
+
padding: 0;
|
|
4
|
+
margin: 0;
|
|
5
|
+
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
|
|
6
|
+
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
a {
|
|
10
|
+
color: inherit;
|
|
11
|
+
text-decoration: none;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
* {
|
|
15
|
+
box-sizing: border-box;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@media (prefers-color-scheme: dark) {
|
|
19
|
+
html {
|
|
20
|
+
color-scheme: dark;
|
|
21
|
+
}
|
|
22
|
+
body {
|
|
23
|
+
color: white;
|
|
24
|
+
background: black;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es5",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"forceConsistentCasingInFileNames": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"module": "esnext",
|
|
12
|
+
"moduleResolution": "node",
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"isolatedModules": true,
|
|
15
|
+
"jsx": "preserve",
|
|
16
|
+
"incremental": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
|
19
|
+
"exclude": ["node_modules"]
|
|
20
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# 🦍 api-ape Examples
|
|
2
|
+
|
|
3
|
+
Complete working examples demonstrating api-ape usage.
|
|
4
|
+
|
|
5
|
+
## Examples
|
|
6
|
+
|
|
7
|
+
| Example | Description | Complexity |
|
|
8
|
+
|---------|-------------|------------|
|
|
9
|
+
| [ExpressJs/](./ExpressJs/) | Basic real-time chat | Minimal setup |
|
|
10
|
+
| [NextJs/](./NextJs/) | Full-featured chat app | Production-ready |
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## ExpressJs — Basic Example
|
|
15
|
+
|
|
16
|
+
A minimal real-time chat demonstrating core api-ape concepts.
|
|
17
|
+
|
|
18
|
+
**Features:**
|
|
19
|
+
- Simple Express.js server with api-ape
|
|
20
|
+
- Broadcast messages to other clients
|
|
21
|
+
- Message history
|
|
22
|
+
|
|
23
|
+
**Quick Start:**
|
|
24
|
+
```bash
|
|
25
|
+
cd ExpressJs
|
|
26
|
+
npm install
|
|
27
|
+
npm start
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Key Files:**
|
|
31
|
+
- `backend.js` — Server setup (22 lines)
|
|
32
|
+
- `api/message.js` — Message handler with `this.broadcastOthers()`
|
|
33
|
+
- `index.html` — Browser client using `window.ape`
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## NextJs — Complete Example
|
|
38
|
+
|
|
39
|
+
A production-ready chat application with Next.js integration.
|
|
40
|
+
|
|
41
|
+
**Features:**
|
|
42
|
+
- Custom Next.js server with api-ape
|
|
43
|
+
- React hooks integration
|
|
44
|
+
- User presence tracking
|
|
45
|
+
- Docker support
|
|
46
|
+
- Connection lifecycle hooks
|
|
47
|
+
|
|
48
|
+
**Quick Start:**
|
|
49
|
+
```bash
|
|
50
|
+
cd NextJs
|
|
51
|
+
npm install
|
|
52
|
+
npm run dev
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Or with Docker:**
|
|
56
|
+
```bash
|
|
57
|
+
cd NextJs
|
|
58
|
+
docker-compose up --build
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Key Files:**
|
|
62
|
+
- `server.js` — Custom Next.js server with api-ape
|
|
63
|
+
- `api/message.js` — Message controller with validation
|
|
64
|
+
- `ape/client.js` — React client wrapper
|
|
65
|
+
- `ape/onConnect.js` — Connection lifecycle hooks
|
|
66
|
+
- `pages/index.tsx` — Chat UI with React hooks
|