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 CHANGED
@@ -24,18 +24,30 @@ yarn add api-ape
24
24
 
25
25
  ## Quick Start
26
26
 
27
- ### Server (Express.js)
27
+ ### Server (Node.js)
28
28
 
29
29
  ```js
30
- const express = require('express')
30
+ const { createServer } = require('http')
31
31
  const ape = require('api-ape')
32
32
 
33
- const app = express()
33
+ const server = createServer()
34
34
 
35
35
  // Wire up api-ape - loads controllers from ./api folder
36
- ape(app, { where: 'api' })
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.listen(3000)
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(app, options)`
101
+ #### `ape(server, options)`
90
102
 
91
- Initialize api-ape on an Express app.
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(app, {
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(app, {
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 Express server allows WebSocket connections from your origin. api-ape uses `express-ws` which handles CORS automatically, but verify your Express CORS middleware allows WebSocket upgrade requests.
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 # Express integration
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
+ })
@@ -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,9 @@
1
+ {
2
+ "name": "bun-chat-example",
3
+ "scripts": {
4
+ "start": "bun run server.ts"
5
+ },
6
+ "dependencies": {
7
+ "api-ape": "file:../.."
8
+ }
9
+ }
@@ -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(app, {
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
- const { online, broadcast } = require('api-ape/server/lib/broadcast')
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 => app.listen(port, () => scribbles.log(`http://localhost:${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
  /**