api-ape 1.1.0 → 2.0.0
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 +24 -11
- package/client/browser.js +7 -1
- package/client/connectSocket.js +35 -3
- package/example/Bun/README.md +74 -0
- package/example/Bun/api/message.ts +11 -0
- package/example/Bun/index.html +76 -0
- package/example/Bun/package.json +9 -0
- package/example/Bun/server.ts +59 -0
- package/example/Bun/styles.css +128 -0
- package/example/ExpressJs/README.md +5 -7
- package/example/ExpressJs/backend.js +23 -21
- package/example/NextJs/ape/client.js +3 -3
- package/example/NextJs/ape/onConnect.js +5 -5
- package/example/NextJs/package-lock.json +1353 -60
- package/example/NextJs/package.json +0 -1
- package/example/NextJs/pages/index.tsx +21 -10
- package/example/NextJs/server.js +7 -11
- package/example/README.md +51 -0
- package/example/Vite/README.md +68 -0
- package/example/Vite/ape/client.ts +66 -0
- package/example/Vite/ape/onConnect.ts +52 -0
- package/example/Vite/api/message.ts +57 -0
- package/example/Vite/index.html +16 -0
- package/example/Vite/package.json +19 -0
- package/example/Vite/server.ts +62 -0
- package/example/Vite/src/App.vue +170 -0
- package/example/Vite/src/components/Info.vue +352 -0
- package/example/Vite/src/main.ts +5 -0
- package/example/Vite/src/style.css +200 -0
- package/example/Vite/src/vite-env.d.ts +7 -0
- package/example/Vite/vite.config.ts +20 -0
- package/index.d.ts +31 -3
- package/package.json +1 -2
- package/server/index.js +10 -2
- package/server/lib/main.js +156 -61
package/README.md
CHANGED
|
@@ -24,18 +24,30 @@ yarn add api-ape
|
|
|
24
24
|
|
|
25
25
|
## Quick Start
|
|
26
26
|
|
|
27
|
-
### Server (
|
|
27
|
+
### Server (Node.js)
|
|
28
28
|
|
|
29
29
|
```js
|
|
30
|
-
const
|
|
30
|
+
const { createServer } = require('http')
|
|
31
31
|
const ape = require('api-ape')
|
|
32
32
|
|
|
33
|
-
const
|
|
33
|
+
const server = createServer()
|
|
34
34
|
|
|
35
35
|
// Wire up api-ape - loads controllers from ./api folder
|
|
36
|
-
ape(
|
|
36
|
+
ape(server, { where: 'api' })
|
|
37
|
+
|
|
38
|
+
server.listen(3000)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**With Express:**
|
|
42
|
+
```js
|
|
43
|
+
const express = require('express')
|
|
44
|
+
const ape = require('api-ape')
|
|
45
|
+
|
|
46
|
+
const app = express()
|
|
47
|
+
const server = app.listen(3000) // Get the HTTP server
|
|
37
48
|
|
|
38
|
-
app
|
|
49
|
+
// Pass the HTTP server (not the Express app)
|
|
50
|
+
ape(server, { where: 'api' })
|
|
39
51
|
```
|
|
40
52
|
|
|
41
53
|
### Create a Controller
|
|
@@ -86,12 +98,13 @@ Include the bundled client and start calling:
|
|
|
86
98
|
|
|
87
99
|
### Server
|
|
88
100
|
|
|
89
|
-
#### `ape(
|
|
101
|
+
#### `ape(server, options)`
|
|
90
102
|
|
|
91
|
-
Initialize api-ape on
|
|
103
|
+
Initialize api-ape on a Node.js HTTP/HTTPS server.
|
|
92
104
|
|
|
93
105
|
| Option | Type | Description |
|
|
94
106
|
|--------|------|-------------|
|
|
107
|
+
| `server` | `http.Server` | Node.js HTTP or HTTPS server instance |
|
|
95
108
|
| `where` | `string` | Directory containing controller files (default: `'api'`) |
|
|
96
109
|
| `onConnent` | `function` | Connection lifecycle hook (see [Connection Lifecycle](#connection-lifecycle)) |
|
|
97
110
|
|
|
@@ -146,7 +159,7 @@ ape.on('notification', ({ data, err, type }) => {
|
|
|
146
159
|
### Default Options
|
|
147
160
|
|
|
148
161
|
```js
|
|
149
|
-
ape(
|
|
162
|
+
ape(server, {
|
|
150
163
|
where: 'api', // Controller directory
|
|
151
164
|
onConnent: undefined // Lifecycle hook (optional)
|
|
152
165
|
})
|
|
@@ -157,7 +170,7 @@ ape(app, {
|
|
|
157
170
|
Customize behavior per connection:
|
|
158
171
|
|
|
159
172
|
```js
|
|
160
|
-
ape(
|
|
173
|
+
ape(server, {
|
|
161
174
|
where: 'api',
|
|
162
175
|
onConnent(socket, req, hostId) {
|
|
163
176
|
return {
|
|
@@ -328,7 +341,7 @@ docker-compose up --build
|
|
|
328
341
|
|
|
329
342
|
### CORS Errors in Browser
|
|
330
343
|
|
|
331
|
-
Ensure your
|
|
344
|
+
Ensure your server allows WebSocket connections from your origin. api-ape uses the `ws` library which handles WebSocket upgrades on the HTTP server level.
|
|
332
345
|
|
|
333
346
|
### Controller Not Found
|
|
334
347
|
|
|
@@ -460,7 +473,7 @@ api-ape/
|
|
|
460
473
|
│ └── connectSocket.js # WebSocket client with auto-reconnect
|
|
461
474
|
├── server/
|
|
462
475
|
│ ├── lib/
|
|
463
|
-
│ │ ├── main.js #
|
|
476
|
+
│ │ ├── main.js # HTTP server integration
|
|
464
477
|
│ │ ├── loader.js # Auto-loads controller files
|
|
465
478
|
│ │ ├── broadcast.js # Client tracking & broadcast
|
|
466
479
|
│ │ └── wiring.js # WebSocket handler setup
|
package/client/browser.js
CHANGED
|
@@ -4,7 +4,7 @@ import connectSocket from './connectSocket.js'
|
|
|
4
4
|
const port = window.location.port || (window.location.protocol === 'https:' ? 443 : 80)
|
|
5
5
|
connectSocket.configure({ port: parseInt(port, 10) })
|
|
6
6
|
|
|
7
|
-
const { sender, setOnReciver } = connectSocket()
|
|
7
|
+
const { sender, setOnReciver, onConnectionChange } = connectSocket()
|
|
8
8
|
connectSocket.autoReconnect()
|
|
9
9
|
|
|
10
10
|
// Global API - use defineProperty to bypass Proxy interception
|
|
@@ -15,3 +15,9 @@ Object.defineProperty(window.ape, 'on', {
|
|
|
15
15
|
enumerable: false,
|
|
16
16
|
configurable: false
|
|
17
17
|
})
|
|
18
|
+
Object.defineProperty(window.ape, 'onConnectionChange', {
|
|
19
|
+
value: onConnectionChange,
|
|
20
|
+
writable: false,
|
|
21
|
+
enumerable: false,
|
|
22
|
+
configurable: false
|
|
23
|
+
})
|
package/client/connectSocket.js
CHANGED
|
@@ -1,9 +1,27 @@
|
|
|
1
1
|
import messageHash from '../utils/messageHash'
|
|
2
2
|
import jss from '../utils/jss'
|
|
3
3
|
|
|
4
|
-
|
|
5
4
|
let connect;
|
|
6
5
|
|
|
6
|
+
// Connection state enum
|
|
7
|
+
const ConnectionState = {
|
|
8
|
+
Disconnected: 'disconnected',
|
|
9
|
+
Connecting: 'connecting',
|
|
10
|
+
Connected: 'connected',
|
|
11
|
+
Closing: 'closing'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Connection state tracking
|
|
15
|
+
let connectionState = ConnectionState.Disconnected
|
|
16
|
+
const connectionChangeListeners = []
|
|
17
|
+
|
|
18
|
+
function notifyConnectionChange(newState) {
|
|
19
|
+
if (connectionState !== newState) {
|
|
20
|
+
connectionState = newState
|
|
21
|
+
connectionChangeListeners.forEach(fn => fn(newState))
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
7
25
|
// Configuration
|
|
8
26
|
let configuredPort = null
|
|
9
27
|
let configuredHost = null
|
|
@@ -82,11 +100,13 @@ const ofTypesOb = {};
|
|
|
82
100
|
function connectSocket() {
|
|
83
101
|
|
|
84
102
|
if (!__socket) {
|
|
103
|
+
notifyConnectionChange(ConnectionState.Connecting)
|
|
85
104
|
__socket = new WebSocket(getSocketUrl())
|
|
86
105
|
|
|
87
106
|
__socket.onopen = event => {
|
|
88
107
|
//console.log('socket connected()');
|
|
89
108
|
ready = true;
|
|
109
|
+
notifyConnectionChange(ConnectionState.Connected)
|
|
90
110
|
aWaitingSend.forEach(({ type, data, next, err, waiting, createdAt, timer }) => {
|
|
91
111
|
clearTimeout(timer)
|
|
92
112
|
//TODO: clear throw of wait for server
|
|
@@ -277,6 +297,7 @@ function connectSocket() {
|
|
|
277
297
|
console.warn('socket disconnect:', event);
|
|
278
298
|
__socket = false
|
|
279
299
|
ready = false;
|
|
300
|
+
notifyConnectionChange(ConnectionState.Disconnected)
|
|
280
301
|
setTimeout(() => reconnect && connectSocket(), 500);
|
|
281
302
|
} // END onclose
|
|
282
303
|
|
|
@@ -531,13 +552,24 @@ function connectSocket() {
|
|
|
531
552
|
reciverOnAr.push(onTypeStFn)
|
|
532
553
|
}
|
|
533
554
|
}
|
|
534
|
-
} // END setOnReciver
|
|
555
|
+
}, // END setOnReciver
|
|
556
|
+
onConnectionChange: (handler) => {
|
|
557
|
+
connectionChangeListeners.push(handler)
|
|
558
|
+
// Immediately call with current state
|
|
559
|
+
handler(connectionState)
|
|
560
|
+
// Return unsubscribe function
|
|
561
|
+
return () => {
|
|
562
|
+
const idx = connectionChangeListeners.indexOf(handler)
|
|
563
|
+
if (idx > -1) connectionChangeListeners.splice(idx, 1)
|
|
564
|
+
}
|
|
565
|
+
}
|
|
535
566
|
} // END return
|
|
536
567
|
} // END connectSocket
|
|
537
568
|
|
|
538
569
|
connectSocket.autoReconnect = () => reconnect = true
|
|
539
570
|
connectSocket.configure = configure
|
|
571
|
+
connectSocket.ConnectionState = ConnectionState
|
|
540
572
|
connect = connectSocket
|
|
541
573
|
|
|
542
574
|
export default connect;
|
|
543
|
-
export { configure };
|
|
575
|
+
export { configure, ConnectionState };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# 🦍 Bun — Example
|
|
2
|
+
|
|
3
|
+
A minimal real-time chat app using Bun's native HTTP server with api-ape.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun install
|
|
9
|
+
bun run start
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Open http://localhost:3000 in multiple browser windows.
|
|
13
|
+
|
|
14
|
+
## Project Structure
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
Bun/
|
|
18
|
+
├── server.ts # Bun server with api-ape (TypeScript)
|
|
19
|
+
├── api/
|
|
20
|
+
│ └── message.ts # Broadcast to other clients
|
|
21
|
+
├── index.html # Chat UI
|
|
22
|
+
└── styles.css # Styling
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## How It Works
|
|
26
|
+
|
|
27
|
+
### Server (server.js)
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
const ape = require('api-ape')
|
|
31
|
+
|
|
32
|
+
const server = Bun.serve({
|
|
33
|
+
port: 3000,
|
|
34
|
+
fetch(req) {
|
|
35
|
+
const url = new URL(req.url)
|
|
36
|
+
if (url.pathname === '/') {
|
|
37
|
+
return new Response(Bun.file('./index.html'))
|
|
38
|
+
}
|
|
39
|
+
return new Response('Not Found', { status: 404 })
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// Pass Bun's server to api-ape
|
|
44
|
+
ape(server, {
|
|
45
|
+
where: 'api',
|
|
46
|
+
onConnent: (socket, req, send) => {
|
|
47
|
+
send('init', { history: [], users: ape.online() })
|
|
48
|
+
ape.broadcast('users', { count: ape.online() })
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
onDisconnent: () => ape.broadcast('users', { count: ape.online() })
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Why Bun?
|
|
58
|
+
|
|
59
|
+
| Feature | Benefit |
|
|
60
|
+
|---------|---------|
|
|
61
|
+
| **No Express needed** | Bun has built-in HTTP server |
|
|
62
|
+
| **Fast startup** | Bun starts in milliseconds |
|
|
63
|
+
| **Native TypeScript** | No build step for TS files |
|
|
64
|
+
| **Smaller footprint** | Fewer dependencies |
|
|
65
|
+
|
|
66
|
+
## Key Concepts Demonstrated
|
|
67
|
+
|
|
68
|
+
| Concept | Example |
|
|
69
|
+
|---------|---------|
|
|
70
|
+
| Auto-wiring | `ape(server, { where: 'api' })` loads `api/*.js` |
|
|
71
|
+
| onConnect hook | `onConnent: (socket, req, send) => { ... }` |
|
|
72
|
+
| Push on connect | `send('init', { history, users })` |
|
|
73
|
+
| Broadcast all | `broadcast('users', { count })` |
|
|
74
|
+
| Broadcast others | `this.broadcastOthers('message', data)` |
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const messages: Array<{ user: string; text: string }> = []
|
|
2
|
+
|
|
3
|
+
// Send message → broadcast to others
|
|
4
|
+
module.exports = function (this: any, data: { user: string; text: string }) {
|
|
5
|
+
messages.push(data)
|
|
6
|
+
this.broadcastOthers('message', data)
|
|
7
|
+
return data
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Export messages for history
|
|
11
|
+
module.exports._messages = messages
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<title>Chat - api-ape</title>
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
|
+
<link rel="stylesheet" href="/styles.css">
|
|
8
|
+
</head>
|
|
9
|
+
|
|
10
|
+
<body>
|
|
11
|
+
<div id="app">
|
|
12
|
+
<div class="chat-container">
|
|
13
|
+
<div class="header">
|
|
14
|
+
<span class="title">💬 Chat</span>
|
|
15
|
+
<span class="online-count">🟢 {{ users }} online</span>
|
|
16
|
+
<span class="user-badge">● {{ user }}</span>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="messages">
|
|
19
|
+
<div v-if="messages.length === 0" class="empty-state">No messages yet...</div>
|
|
20
|
+
<div v-for="(m, i) in messages" :key="i" :class="['message', m.user === user ? 'mine' : '']">
|
|
21
|
+
<span class="username">{{ m.user }}</span>
|
|
22
|
+
<span class="text">{{ m.text }}</span>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
<div class="input-area">
|
|
26
|
+
<input class="message-input" placeholder="Type a message..." v-model="input" @keydown.enter="send" autofocus>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<script src="https://unpkg.com/vue@3"></script>
|
|
32
|
+
<script src="/api/ape.js"></script>
|
|
33
|
+
<script>
|
|
34
|
+
const { createApp, ref, onMounted } = Vue
|
|
35
|
+
|
|
36
|
+
createApp({
|
|
37
|
+
setup() {
|
|
38
|
+
const user = ref('User' + Math.random().toString(36).slice(2, 6))
|
|
39
|
+
const users = ref(0)
|
|
40
|
+
const messages = ref([])
|
|
41
|
+
const input = ref('')
|
|
42
|
+
|
|
43
|
+
onMounted(() => {
|
|
44
|
+
// Receive init with history + user count on connect
|
|
45
|
+
ape.on('init', ({ data }) => {
|
|
46
|
+
messages.value = data.history || []
|
|
47
|
+
users.value = data.users || 0
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// Listen for user count updates
|
|
51
|
+
ape.on('users', ({ data }) => {
|
|
52
|
+
users.value = data.count
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// Listen for new messages from others
|
|
56
|
+
ape.on('message', ({ data }) => {
|
|
57
|
+
messages.value.push(data)
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
function send() {
|
|
62
|
+
if (!input.value) return
|
|
63
|
+
const msg = { user: user.value, text: input.value }
|
|
64
|
+
ape.message(msg).then(() => {
|
|
65
|
+
messages.value.push(msg)
|
|
66
|
+
})
|
|
67
|
+
input.value = ''
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { user, users, messages, input, send }
|
|
71
|
+
}
|
|
72
|
+
}).mount('#app')
|
|
73
|
+
</script>
|
|
74
|
+
</body>
|
|
75
|
+
|
|
76
|
+
</html>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bun server using api-ape library (TypeScript)
|
|
3
|
+
* Bun natively supports TypeScript - no build step needed!
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createServer, IncomingMessage, ServerResponse } from 'http'
|
|
7
|
+
import path from 'path'
|
|
8
|
+
import fs from 'fs'
|
|
9
|
+
import ape from 'api-ape'
|
|
10
|
+
|
|
11
|
+
const port = parseInt(process.env.PORT || '3000', 10)
|
|
12
|
+
|
|
13
|
+
// Create HTTP server
|
|
14
|
+
const server = createServer((req: IncomingMessage, res: ServerResponse) => {
|
|
15
|
+
const url = new URL(req.url || '/', `http://localhost:${port}`)
|
|
16
|
+
|
|
17
|
+
if (url.pathname === '/') {
|
|
18
|
+
res.writeHead(200, { 'Content-Type': 'text/html' })
|
|
19
|
+
res.end(fs.readFileSync(path.join(__dirname, 'index.html')))
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (url.pathname === '/styles.css') {
|
|
24
|
+
res.writeHead(200, { 'Content-Type': 'text/css' })
|
|
25
|
+
res.end(fs.readFileSync(path.join(__dirname, 'styles.css')))
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
res.writeHead(404)
|
|
30
|
+
res.end('Not Found')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
// Initialize api-ape
|
|
34
|
+
ape(server, {
|
|
35
|
+
where: 'api',
|
|
36
|
+
onConnent: (socket, req, send) => {
|
|
37
|
+
const messageModule = require('./api/message')
|
|
38
|
+
setTimeout(() => {
|
|
39
|
+
send('init', { history: messageModule._messages, users: ape.online() })
|
|
40
|
+
ape.broadcast('users', { count: ape.online() })
|
|
41
|
+
}, 100)
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
onDisconnent: () => ape.broadcast('users', { count: ape.online() })
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
server.listen(port, () => {
|
|
50
|
+
console.log(`
|
|
51
|
+
╔═══════════════════════════════════════════════════════╗
|
|
52
|
+
║ 🦍 api-ape Bun Example (TypeScript) ║
|
|
53
|
+
╠═══════════════════════════════════════════════════════╣
|
|
54
|
+
║ HTTP: http://localhost:${port}/ ║
|
|
55
|
+
║ WebSocket: ws://localhost:${port}/api/ape ║
|
|
56
|
+
║ Runtime: Bun 🥖 + TypeScript ║
|
|
57
|
+
╚═══════════════════════════════════════════════════════╝
|
|
58
|
+
`)
|
|
59
|
+
})
|
|
@@ -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
|
+
#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
|
+
}
|
|
@@ -33,25 +33,23 @@ npm i api-ape
|
|
|
33
33
|
```js
|
|
34
34
|
const express = require('express')
|
|
35
35
|
const ape = require('api-ape')
|
|
36
|
-
const { online, broadcast } = require('api-ape/server/lib/broadcast')
|
|
37
36
|
|
|
38
37
|
const app = express()
|
|
38
|
+
const server = app.listen(3000)
|
|
39
39
|
|
|
40
|
-
ape(
|
|
40
|
+
ape(server, {
|
|
41
41
|
where: 'api',
|
|
42
42
|
onConnent: (socket, req, send) => {
|
|
43
43
|
// Push history + user count on connect
|
|
44
44
|
const { _messages } = require('./api/message')
|
|
45
|
-
send('init', { history: _messages, users: online() })
|
|
46
|
-
broadcast('users', { count: online() })
|
|
45
|
+
send('init', { history: _messages, users: ape.online() })
|
|
46
|
+
ape.broadcast('users', { count: ape.online() })
|
|
47
47
|
|
|
48
48
|
return {
|
|
49
|
-
onDisconnent: () => broadcast('users', { count: online() })
|
|
49
|
+
onDisconnent: () => ape.broadcast('users', { count: ape.online() })
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
})
|
|
53
|
-
|
|
54
|
-
app.listen(3000)
|
|
55
53
|
```
|
|
56
54
|
|
|
57
55
|
### Controller (api/message.js)
|
|
@@ -1,32 +1,15 @@
|
|
|
1
1
|
const express = require('express')
|
|
2
|
-
const scribbles = require('scribbles')
|
|
3
|
-
const net = require('net')
|
|
4
2
|
const path = require('path')
|
|
5
3
|
const ape = require('api-ape')
|
|
6
4
|
|
|
7
5
|
const app = express()
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
ape(app, {
|
|
12
|
-
where: 'api',
|
|
13
|
-
onConnent: (socket, req, send) => {
|
|
14
|
-
// Send history + user count on connect
|
|
15
|
-
const { _messages } = require('./api/message')
|
|
16
|
-
setTimeout(() => {
|
|
17
|
-
send('init', { history: _messages, users: online() })
|
|
18
|
-
broadcast('users', { count: online() })
|
|
19
|
-
}, 100)
|
|
20
|
-
|
|
21
|
-
return {
|
|
22
|
-
onDisconnent: () => broadcast('users', { count: online() })
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
})
|
|
26
|
-
|
|
7
|
+
// Serve static files
|
|
27
8
|
app.get('/', (req, res) => res.sendFile(path.join(__dirname, 'index.html')))
|
|
28
9
|
app.get('/styles.css', (req, res) => res.sendFile(path.join(__dirname, 'styles.css')))
|
|
29
10
|
|
|
11
|
+
// Find available port
|
|
12
|
+
const net = require('net')
|
|
30
13
|
const findPort = (port, cb) => {
|
|
31
14
|
const server = net.createServer()
|
|
32
15
|
server.once('error', () => findPort(port + 1, cb))
|
|
@@ -34,4 +17,23 @@ const findPort = (port, cb) => {
|
|
|
34
17
|
server.listen(port)
|
|
35
18
|
}
|
|
36
19
|
|
|
37
|
-
findPort(3000, port =>
|
|
20
|
+
findPort(3000, port => {
|
|
21
|
+
// Create HTTP server from Express app
|
|
22
|
+
const server = app.listen(port, () => console.log(`http://localhost:${port}`))
|
|
23
|
+
|
|
24
|
+
// Initialize api-ape with the HTTP server
|
|
25
|
+
ape(server, {
|
|
26
|
+
where: 'api',
|
|
27
|
+
onConnent: (socket, req, send) => {
|
|
28
|
+
const { _messages } = require('./api/message')
|
|
29
|
+
setTimeout(() => {
|
|
30
|
+
send('init', { history: _messages, users: ape.online() })
|
|
31
|
+
ape.broadcast('users', { count: ape.online() })
|
|
32
|
+
}, 100)
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
onDisconnent: () => ape.broadcast('users', { count: ape.online() })
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
})
|
|
@@ -45,15 +45,15 @@ async function initClient() {
|
|
|
45
45
|
const port = window.location.port || (window.location.protocol === 'https:' ? 443 : 80)
|
|
46
46
|
connectSocket.configure({ port: parseInt(port, 10) })
|
|
47
47
|
|
|
48
|
-
// Connect and get sender/receiver
|
|
49
|
-
const { sender, setOnReciver } = connectSocket()
|
|
48
|
+
// Connect and get sender/receiver/connection state
|
|
49
|
+
const { sender, setOnReciver, onConnectionChange } = connectSocket()
|
|
50
50
|
|
|
51
51
|
// Enable auto-reconnect
|
|
52
52
|
connectSocket.autoReconnect()
|
|
53
53
|
|
|
54
54
|
console.log('🦍 api-ape client initialized')
|
|
55
55
|
|
|
56
|
-
return { sender, setOnReciver, connectSocket }
|
|
56
|
+
return { sender, setOnReciver, onConnectionChange, connectSocket }
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
/**
|